Obiekty i referencje


1. Obiekty i referencje

Rozważmy jedną z możliwych wersji klasy Para. Jej obiekty są parami liczb całkowitych tzn. każdy obiekt składa się z dwóch elementów - liczb całkowitych (pierwszego składnika pary i drugiego składnika pary).
To co możemy robić z obiektami klasy Para określone jest przez zestaw metod tej klasy (poleceń, które można wydawać obiektom-parom liczb całkowitych). W  tej wersji klasy Para zdefiniujemy następujące metody:

public class Para {
  private int a;
  private int b;
 
  // konstruktor bezparametrowy
  public Para() { 
  }
 
  // konstruktor z dwoma argumentami = elementami pary
  public Para(int x, int y) { 
    a = x;
    b = y;
  }
 
  // Metoda ustalająca składniki pary 
  public void set(int x, int y) { 
    a = x;
    b = y;
  }
 
  // Metoda pokazujaca parę
  public void show() {
    System.out.println("( " + a + "," + b + " )");
  }

}
Co trzeba zrobić, żeby ustalić wartość pary i wyprowadzić ją na konsolę?


Może to wyglądac w następujący sposób:
public class ParaSetAndShow {

  public static void main(String[] args) {

    Para para1 = new Para(); // 1
    para1.set( 1, 2 );       // 2
    para1.show();            // 3

  }
}
Zobacz demonstrację działania  programu.


Wydruk dzialania programu:
( 1 , 2 )

Po kolei:
Nieco niepokojące jest w tym opisie ciagłe używanie sformułowania "obiektu oznaczanego przez (zmienną) para1". Czyż nie łatwiej byłoby mowić: obiektu para1, tak jak mówimy np liczby calkowitej x? I w ogóle czym jest, tak naprawdę, zmienna para1? 
Co by się stało gdybyśmy po prostu napisali:

Para p;
p.set( 1, 2 );
p.show();

Widzimy tu deklarację zmiennej p, mówiąca o tym, że jest ona typu Para.
To jest bardzo podobne do deklaracji zmiennych typów prostych.

int x;    // deklaracja zmiennej typu int
Para p; // deklaracja zmiennej para1, za pomocą której możemy operować na obiektach klasy Para

Pomiędzy tymi deklaracjami występuje jednak subtelna różnica znaczeniowa.

Otóż, deklaracja zmiennej x wydziela pamięć dla przechowywania liczby całkowitej (cztery bajty). W tym momencie x jest synonimem jednostki danych - liczby całkowitej.
Piszemy np. x = 4; i do miejsca pamięci oznaczanego przez zmienną x wpisywana jest liczba 4. Wygląda to mniej więcej tak:
r

Zatem sama deklaracja zmiennej calkowitoliczbowej x tworzy "obiekt" - liczbę całkowitą.

W przypadku deklaracji zmiennej, która będzie oznaczać obiekt jakiejś klasy sytuacja jest zupełnie inna. Deklaracja nie tworzy obiektu (nie wydziela pamięci do przechowywania obiektu klasy).

Sam obiekt musi być dopiero utworzony - za pomocą wyrażenia new.


Jego zastosowanie powoduje przydzielenie pamięci dla obiektu w dynamicznej (zmieniającej się w trakcie działania programu) części pamięci, zwanej stertą.
Wynikiem wyrazenia new jest adres (lokalizacja) miejsca w pamięci, przydzielonego obiektowi. Ten adres możemy przypisać zmiennej za pomocą której chcemy na danym obiekcie operować.

Np. deklaracja:

Para p;

nie tworzy obiektu klasy Para.

A jeśli nie ma obiektu, to nie możemy posłać do niego żadnego komunikatu (wydać polecenia, wywołać na jego rzecz metody). Zatem, w tym kontekście, p.set(...) i p.show() będą niepoprawnymi odwołaniami i spowodują błąd wykonania programu tzw. NullPointerException (odwołanie do nieistniejącego obiektu)

Zatem zmienna p nie zawiera obiektu Para.
Może natomiast zawierać jego lokalizację (adres w pamięci) - inaczej nazywaną referencją do obiektu.

Referencja to wartość, która oznacza lokalizację (adres) obiektu w pamięci


Obiekt klasy Para możemy utworzyć używając wyrażenia new Para(), a przypisując wartość tego wyrażenia zmiennej p, uzyskujemy możliwość operowania na tym obiekcie:

Para p;
p = new Para();

co wcześniej, w skrócie, zapisywaliśmy stosując inicjację przy deklaracji:

Para p = new Para();


Dokladnego wyjaśnienia dostarcza poniższy ideowy schemat.

r 

gdzie:
  1. Przydzielenie pamięci zmiennej p do przechowania referencji do obiektu (4 bajty w pamięci). Referencja jest nieustalona, ma wartość null, co oznacza, że nie odnosi się do żadnego obiektu.
  2. Opracowanie wyrażenia new powoduje przydzielenie pamięci dla obiektu klasy Para na stercie pod jakimś wolnym adresem (tu symbolicznie 1304). Wielkość przydzielonego obszaru jest wystarczająca, by zmieścić dwie liczby całkowite (składniki pary). W tym momencie oba składniki pary równe są 0.
  3. Wartością wyrażenia new jest referencja (adres 1304). Jest ona umieszczana w uprzednio przydzielonym zmiennej p obszarze pamięci
  4. Zmienna p ma teraz wartość = referencji do obiektu klasy Para, któremu w kroku 2 przydzielono pamięć na stercie (adres 1304).
Zatem zmienna p w naszym przykładzie zawiera (w końcu) referencję do obiektu klasy Para. Powiemy też "wskazuje na obiekt". Powiemy też czasem w skrócie: jest referencją.

Mówiliśmy wcześniej: "zmienna p może oznaczać obiekt klasy Para" w tym właśnie sensie, iż może zawierać referencję do obiektu klasy Para (zatem jakoś go "oznaczać", ale na pewno nie zawierać). A dlaczego może? Bo nie zawsze zawiera referencję do obiektu, czasami (np. zaraz po deklaracji bez inicjacji) nie zawiera referencji do żadnego obiektu (bo żaden nie został jeszcze utworzony).

Powstaje pytanie - jakiego typu jest zmienna p i wszystkie podobne zmienne, te o których mówiliśmy, że mogą oznaczać obiekty?

Otóż w Javie oprócz typów numerycznych i typu boolowskiego istnieje jeszcze tylko jeden typ - typ referencyjny.

Wszystkie zmienne deklarowane z nazwą klasy w miejscu nazwy typu są zmiennymi typu referencyjnego. Zmienne te mogą zawierać referencje do obiektów lub nie zawierać żadnej referencji (nie wskazywać na żaden obiekt).


Wartość zmiennej typu referencyjnego, która nie zawiera referencji do obiektu równa jest null. Słowo null jest słowem kluczowym języka

Zatem dopuszczalne wartości zmiennych typu referencyjnego - to referencje do obiektów danej klasy lub wartośc null. Tak samo jak 1 jest literałem typu int - null jest literałem typu referencyjnego.

Referencje są bardzo podobne do wskaźników w C, z tą istotną różnicą, że nie ma w Javie arytmetyki "na referencjach". Dzięki temu programowanie w Javie jest bardziej odporne na błędy. Arytmetyka wskaźnikowa w C jest częstą przyczyną błędów, gdyż pozwala sięgać do dowolnego miejsca w pamięci (np. poprzez zwiększanie wskaźnika, który wskazuje na obszar przydzielony jakiejś zmiennej).
Dla wartości typów referencyjnych (które to wartości w istocie są liczbami, bo adresy obiektów są liczbami) nie są dopuszczalne operacje arytmetyczne. Możemy natomiast:
Musimy zawsze pamiętać, że operacje te (wykonywane na zmiennych, oznaczających obiekty) dotyczą referencji, a nie obiektów (na obiektach, ich wnętrzu operujemy za pomocą metod, poleceń posyłanych do obiektów za pośrednictwem referencji i za pomocą "operatora" kropka).

Wyobraźmy sobie, że na dwóch "danych" - liczbach całkowitych i na dwóch "danych" - obiektach klasy Para wykonujemy podobne operacje:
  1. Nadanie wartości pierwszej danej, nadanie wartości drugiej danej.
  2. Przypisanie zmiennej oznaczającej pierwszą daną wartości zmienej oznaczającej drugą daną
  3. Zmianę wartości drugiej danej.
  4. Porównanie zmiennych, oznaczających obie dane
A dodatkowo (w obu przypadkach) wprowadzimy trzecią daną, której wartość ustalimy na wartość drugiej i porównamy zmienne oznaczające te dane (drugą i trzecią).
Program mógłby wyglądać tak:

public class Roznica {

  public static void main(String[] args) {

    // Operacje na zmiennych typów prostych
    int x, y, z;
    x = 3;
    y = 4;
    x = y;
    y = 5;
    z = 5;
    System.out.println("x = " + x);
    System.out.println("y = " + y);
    System.out.println("z = " + z);
    if (x == y) System.out.println ("x i y równe.");
    else System.out.println ("x i y nierówne.");
    if (y == z) System.out.println ("y i z równe.");
    else System.out.println ("y i z nierówne."); 

    // Podobne operacje na zmiennych typu referencyjnego
    Para px = new Para(), py = new Para(), pz = new Para();
    px.set( 3, 3 );
    py.set( 4, 4 );
    pz.set( 5, 5 );
    px = py;
    py.set( 5, 5 );
    System.out.print("Para px: "); px.show();
    System.out.print("Para py: "); py.show();
    System.out.print("Para pz: "); pz.show();
    if (px == py) System.out.println ("px i py równe.");
    else System.out.println ("px i py nierówne.");
    if (py == pz) System.out.println ("py i pz równe.");
    else System.out.println ("py i pz nierówne.");

  }
}
Zobacz demonstrację działania programu.

Uwaga: w programie zastosowano instrukcję if-else. W instrukcji tej sprawdzany jest warunek w nawiasach i jeśli jest prawdziwy wykonywana jest część po if; jeśli nie - część po słowie else. Więcej na ten temat w następnej części tekstu.

Wynik działania programu pokazuje następujący wydruk:

x = 4
y = 5
z = 5
x i y nierówne.
y i z równe.
Para px: ( 5 , 5 )
Para py: ( 5 , 5 )
Para pz: ( 5 , 5 )
px i py równe.
py i pz nierówne.

Wynik działania programu może wyglądac zaskakująco dla kogoś, kto nie uświadomi sobie braku różnicy pomiędzy operacjami na zmiennych typów prostych i referencyjnych (myśląc że zmienne typów referencyjnych zawierają obiekty). Otrzymany rezultat wynika z następujących faktów:
Przy okazji warto zastanowić się, co dzieje się obiektem-parą o wartościach (3,3) na którą wskazywała najpierw referencja px. Obiekt ten został utworzony na stercie (px = new Para()), a więc zajmuje jakiś obszar pamięci. Następnie ustalono wartości jego elementów (składników pary) - px.set(3,3) - a więc te wartości zostały wpisane do tego obszaru. Po czym zmiennej px przypisano wartość zmiennej py i w ten sposób w programie nie mamy już żadnej referencji do tego obiektu. A ponieważ na obiektach możemy działać tylko za pomocą referencji, to jest on już dla nas bezużyteczny i wyłącznie "zaśmieca pamięć". Czy musimy się tym martwić? Gdyby np. takich zaśmiecających pamięć obiektów pojawiło się w naszym programie tysiące, to czy nie spowodowałoby to przepełnienia pamięci?

Jest to istotne ułatwienie w porównaniu z takimi językami jak C czy C++, gdzie dynamicznie alokowane przez programistę (za pomocą operatorów lub funkcji) obszary pamięci muszą być przez programistę świadomie zwalniane
Na szczęście - nie. Bezużyteczne obiekty są automatycznie usuwane z pamięci (bez konieczności żadnej ingerencji programisty), mimo, że powstały one na skutek wykonania odpowiednich instrukcji zapisanych przez programistę w programie (np. Para px = new Para();).




Obiekty, na które w programie nie wskazuje już żadna referencja są automatycznie usuwane z pamięci. Nazywa się to automatycznym odśmiecaniem (garbage collecting)


Podsumujmy najważniejsze fakty.




Zaletą takiego modelu jest to, że przypisania referencji nie kopiują danych. Cóż by się stało, gdyby było inaczej?
Np. niech x wskazuje na obiekt-obraz  o rozmiarze 10MB. Przypisanie: y = x (zakładając, że zmienne x i y są tego samego typu) powieliłoby obraz i mielibyśmy dodatkowe 10MB zajętej pamięci. W Javie nic takiego się nie dzieje: przypisanie y=x kopiuje referencję (adres), a 10 MB obraz jest tylko jeden.

Jednak model jest trudny w opisie. Precyzyjne opisy działania programu (ściśle zgodne z modelem) muszą roić się od słabo czytelnych zbitek.
Na przykład, opis takiego fragmentu:

JButton b = new JButton("Ok");
Color c = b.getBackground();

powinien wyglądac tak:

"zmiennej c przypisujemy referencję do obiektu klasy Color zwróconą przez metodę getBackground(), wywołaną na rzecz obiektu klasy JButton, do którego referencję zawiera zmienna b".

Dlatego w dalszych częściach tekstu  będziemy się starali używać uproszczonego języka, stosując swoiste skróty myślowe.

Zawsze pamiętając o różnicy między referencją, zmienną zawierającą referencję oraz obiektem będziemy czasem (dla uproszczenia opisów) mówić:


Na przykład w kontekście:

String txt;
...
powiemy czasem: "łańcuch znakowy txt" lub "napis txt"

a w kontekście :

Color c = b.getBackground();

powiemy czasem: "uzyskanie koloru tła przycisku b"

Należy pamiętać, że będą to wszystko skróty myślowe, służące do bardziej klarownego przedstawienia treści, istoty, wysokopoziomowej semantyki programów.

2. Napisy

Oczywiście, napisy (łańcuchy znakowe) są obiektami klasy String, zatem wszystko co dotąd powiedziano o obiektach i referencjach dotyczy także obiektów klasy String.
Dodatkowo jednak, ponieważ operacje na łańcuchach znakowych są w programowaniu dość częste, kompilator dostarcza nam tu pewnych udogodnień.

Powtórzmy sobie znowu.
Gdy napiszemy

String napis;

to w tym momencie nie będzie jeszcze żadnego łańcucha znakowego, jedynie zmienna napis, która może zawierać referencję do obiektu (ale jeszcze nie zawiera).

Tak jak w przypadku każdej innej klasy obiekty klasy String musimy bezpośrednio tworzyć.
Pierwsza udogodnienie polega na tym, że zamiast:

String s = new String("Ala ma kota");

możemy napisać:

String s = "Ala ma kota";

Zapis ten spowoduje:
Zatem, wyjątkowo, tworząc obiekty klasy String nie musimy używac wyrażenia new.


Innym (wyjątkowym) udogodnieniem przy korzystaniu z łańcuchów znakowych jest możliwość użycia operatora + w znaczeniu konkatenacji (łączenia łańcuchów znakowych).
Np.
String s1 = "Ala ma kota";
String s2 = " szaroburego";
String s3;
s3 = s1 + s2;
spowoduje, że:

Takie zastosowanie operatora + jest wyjątkowe, gdyż (wizualnie) stosujemy go wobec referencji (np. s1 + s2), co ogólnie  jest niedozwolone. 

Co więcej, za pomocą operatora + do łańcuchów znakowych możemy dołączać innego rodzaju dane, np. liczby (a także dane reprezentujące obiekty dowolnych klas).
Na przykład:

String s1 = "Ala ma kota";
String s2 = " szaroburego";
String s3;
s3 = s1 + s2 + " w wieku " + 3 + " lat ";
Teraz zmienna s3 będzie oznaczać napis "Ala ma kota szaroburego w wieku 3 lat".
Oczywiście, nic nie stoi na przeszkodzie, by w konkatenacji zamiast literału 3 pojawiła się zmienna typu int o wartości 3. Np.

int lata = 3;
...
s3 = s1 + s2 + " w wieku " + lata + " lat ";

Zwróćmy uwagę, zmienna lata lub literał 3 w wyrażeniu konkatenacji łańcuchów znakowych są typu int. Przy opracowaniu wyrażenia (wyliczeniu jego wyniku) następuje przekształcenie wartości zmiennej lub literału (dziesiętne 3, binarne 00000011 - to jest tzw. wewnętrzna reprezentacja wartości) w kod znaku Unicodu (dziesiętnie 33) i dzięki temu znak cyfry ('3') pojawi się w łańcuchu znakowym (znak cyfry 3 jest znakową reprezentacją wartości 3).
To samo dotyczy innych wartości numerycznych (typów float, double, itp.)

Jeśli w wyrażeniu konkatenacji łańcuchow znakowych wystąpi referencja do obiektu jakiejś klasy, to  obiekt (dane zawarte w obiekcie) zostaną za pomocą metody toString() przekształcone do postaci znakowej i dołączone do łańcucha. 

Na przykład jeśli napiszemy:

Para p = new Para(10,11);
String s = "Ta para jest równa" + p;

to w wyrażeniu konkatenacji  zostanie automatycznie wywołana metoda toString() z klasy Para i dostaniemy w wyniku napis:
Ta para jest równa ( 10, 11)

Należy także zwrócić uwagę na dwie ważne kwestie:

Po pierwsze, operator + jest traktowany jako operator konkatenacji łańcuchów znakowych tylko wtedy, gdy jeden z jego argumentów jest typu String


Zatem np. takie fragmenty będą niepoprawne:

String s = 1 + 3;
wynikiem prawej strony operatora przypisania jest liczba 4 (typ int), a danej typu int nie można podstawić na zmienną typu referencyjnego (którą jest s)

int a = 1, b = 3;
String s = a + b;
j.w.


Po drugie, przy konkatenacji należy baczną uwagę zwracać na kolejność opracowywania wyrażeń


Np.

String s = "Nr " + 1 + 2;

da napis "Nr 12", bo: najpierw zostanie wyliczone wyrażenie "Nr " + 1, co w wyniku da napis "Nr 1", po czym drugi operator + dołączy do tego napisu napis "2" (znakową wartość liczby 2).

Natomiast:

String s = 100 + " Nr " + (1 +2);

da napis "100 Nr 3", bo:

Przy operowaniu na łańcuchach znakowych trzeba szczególnie pamiętać, że dostęp do nich uzyskujemy za pomocą referencji, co ma swoje konsekwencje przy operacjach porównania na równość - nierówność.

Jeszcze raz:

Operatory równości (==) i nierówności (!=) zastosowane wobec zmiennych oznaczających obiekty , porównują referencje do obiektów, a nie zawartość obiektów


Zatem poniższy fragment:
String s1 = "Al";
String s2 = "a";
String s3 = s1 + s2;
String s4 = "Ala";
System.out.println(s3 + " " + s4);
if (s3 == s4) System.out.println("To jest Ala");
else System.out.println("To nie Ala"

wyprowadzi (wbrew intuicyjnym oczekiwaniom):
Ala Ala
To nie Ala

Zwróćmy uwagę: zawartością obiektu oznaczanego przez s3 jest napis "Ala".
Również - zawartością obiektu oznaczanego przez s4 jest taki sam napis "Ala".
Ale porównanie obu zmiennych da wartość false, bo s3 wskazuje na inny obiekt niż s4.
Porównanie byłoby prawdziwe tylko wtedy, gdyby s3 wskazywało na ten sam obiekt co s4.

Do porównywania łańcuchów znakowych (ich zawartości) nie należy używać operatorów == i !=


Ktoś mógłby powiedzieć: czyżby?
A co z takim fragmentem programu?
String s1 = "Ala";
String s2 = "Ala";
if (s1 == s2) System.out.println("To jest Ala");
Cóż - wyprowadzi on napis "To  jest Ala", jakby wbrew sformułowanej przed chwilą regule.
Ale dzieje się tak tylko dlatego, że  wszystkie literały łańcuchowe mające ten sam tekst, są jednym i tym samym obiektem. Obiekty-literały są tworzone w fazie kompilacji i dodawane do puli literałów. W instrukcji: s1 = "Ala"; zmiennej s1 jest przypisywana referencja do literału "Ala". To samo dzieje się w instrukcji s2 = "Ala"; zatem obie zmienne wskazują ten sam obiekt.
Dlatego  porównanie da wartośc true.

Jednak nie należy wykorzystywać tej właściwości języka i zawsze, zamiast operatora ==  należy stosować polecenie equals

3. Interakcja: wprowadzanie napisów i liczb, komunikaty

W realnych sytuacjach programy komunikują się z użytkownikiem, umożliwiając wprowadzanie danych i wyprowadzając wyniki. Wiemy już jak wypisać wyniki na konsoli.
Ale jak wprowadzić dane do programu? I jak pokazać wyniki w trochę wygodniejszej i ładniejszej od konsolowego wyjścia formie?
Do wprowadzania danych możemy użyć:
Do pokazywania wyników w okienku komunikatów możemy użyć statycznej metody showMessageDialog klasy JOptionPane z pakietu javax. swing;

Zapis w programie:

    String s = JOptionPane.showInputDialog("Komunikat");

spowoduje otwarcie okienka dialogowego z komunikatem "Komunikat", w którym będzie można wprowadzić jakiś tekst. Po kliknięciu w przycisk Ok wprowadzony łańcuch znakowy będzie dostępny za pomocą zmiennej s. Zamknięcie dialogu (np. przyciskiem Cancel) spowoduje, że zmienna s będzie miała wartośc null.

Zapis:

    JOptionPane.showMessageDialog(null, "Komunikat");

pokaże okno dialogowe z komunikatem "Komunikat" (w tym zapisie null mówi m.in. o tym, że okno dialogowe ma być wycentrowany w obszarze pulpitu graficznego interfejsu systemu operacyjnego; zamiast null można podać referencję do obiektu-okna innego od pulpitu)

Przykład wykorzystania dialogów w programie:
import javax.swing.JOptionPane;

public class GreetMsg {

  public static void main(String[] args) {

    String name = JOptionPane.showInputDialog("Podaj swoje imię");
    if (name == null) name = "";
    JOptionPane.showMessageDialog( null, "Witaj " + name + "!");

  }

}
Demo:
 
Program uwidoczni dialog wejściowy, w którym będziemy mogli wprowadzić tekst. Gdy po wpisaniu tekstu klikniemy "Ok", to zmienna name będzie wskazywać na napis, który wprowadziliśmy w dialogu. Jeśli natomiast  zamkniemy dialog, albo klikniemy "Cancel", to wynikiem odwołania JOptionPane.showInputDialog(...) będzie null i tę właśnie wartość będzie miala zmienna name (co oznacza, że nie wskazuje na żaden łańcuch znakowy).
W takim przypadku przypiszemy jej referencję do pustego łańcucha znakowego "".
Poniższe rysunki ilustrują dzialanie programu.
Pytanie:
r
Wynik w okienku komunikatów:
r

Dane możemy także wprowadzać z konsoli (standardowego wejścia). W Javie oznaczane jest ono przez statyczną publiczną stałą z klasy System: System.in. Wygodnym sposobem czytania ze standardowego wejścia jest użycie klasy Scanner. Pokazuje to przykładowy program:

import java.util.*;

public class ScanString {

  public static void main(String[] args) {
    Scanner scan = new Scanner(System.in);
    System.out.println("Podaj imię");
    String name = scan.next();
    System.out.println("Witaj " + name);

  }

}
Tworząc obiekt klasy Scanner podajemy w konstruktorze źródło danych. Tutaj jest nim standardowe wejście (System.in). Metoda next() wczytuje ze standardowego wejścia kolejny symbol (napis nie zawierający tzw. białych znaków, czyli spacji, tabulacji, znaku nowego wiersza itp.).
Uzyskamy podobny efekt, jak w poprzednim programie, tyle, że za pośrednictwem konsoli.

A jak wprowadzić liczby?
W przypadku skanera jest to dość proste, ma on bowiem metody do wprowadzania liczb, przekształcające ich znakową postać (napisy oznaczające liczby) w postać binarną.
Obrazuje to poniższy program:
import java.util.*;

public class ScanNums {

  public static void main(String[] args) {
    Scanner scan = new Scanner(System.in);
    scan.useLocale(new Locale("en"));
    System.out.println("Wprowadż liczbę całkowitą:");
    int i = scan.nextInt();
    System.out.println("Wprowadż liczbę rzeczywistą:");
    double d = scan.nextDouble();
    System.out.println("Wprowadzono: " + i + " " + d);
  }

}
Metoda nextInt() pobiera ze źródła danych kolejną liczbę całkowitą, nextDouble() - rzeczywistą.
Skaner jest wrażliwy na ustawienia regionalne -tzw. locale (m.in. to czy separatorem miejsc dzisiętnych w liczbach rzeczywistych jest kropka czy przecinek). Dlatego w programie użyto metody useLocale(...) zapewniającej użycie separatora właściwego dla  ustawień (lokalizacji) języka angielskiego ("en"), czyli kropki. W lokalizacji polskiej, wprowadzając dane, musielibyśmy używać przecinka.

A jak wprowadzić do programu liczby przy zastosowaniu dialogów wejściowych?
Gdy w polu tekstowym okna dialogowego wpiszemy jakąś liczbę, to będzie ona w programie dostępna jako napis (łańcuch znakowy, String). Oczywiście nie możemy na takim napisie wykonywać operacji arytmetycznych. Do tego potrzebne jest przekształcenie znakowej (napisowej) reprezentacji liczby do postaci binarnej (takiej, w jakiej liczby są zapisywane w pamięci komputera). Skaner robi to automatycznie, stosując dialogi sami musimy zadbać o odpowiednią konwersję.

Do przekształcania napisów na liczby całkowite można wykorzystać następujące odwołanie:

            liczba = Integer.parseInt(napis);

gdzie:
        liczba - jest dowolną zmienną typu int,
        napis  - jest wyrażeniem typu String.



Oczywiście, nie każdy napis reprezentuje liczbę całkowitą. Jeśli np. w napisie znajdzie się znak litery lub kropka dziesiętna, to przy próbie przekształcenia napisu na liczbę całkowitą wystąpi błąd. W Javie taki błąd sygnalizowany jest za pomoca wyjątku, który nazywa się NumberFormatException.
Dopóki  nie poznamy mechanizmu obsługi błędów (wyjątków) musimy się liczyć z tym, że w takim przypadku wykonanie programu zostanie przerwane. To samo dotyczy skanera, z tym, że zgłaszany jest błąd - wyjątek InpuMismatchException.

Przykład wprowadzania liczb calkowitych w dialogach wejściowych: program sumujący dwie wprowadzone  liczby.

import javax.swing.*;

public class ParseInt {

  public static void main(String[] args) {

    String s1 = JOptionPane.showInputDialog("Podaj pierwszą liczbę");
    if (s1 != null) {
      String s2 = JOptionPane.showInputDialog("Podaj drugą liczbę");
      if (s2 != null) {
         int l1 = Integer.parseInt(s1);
         int l2 = Integer.parseInt(s2);
         JOptionPane.showMessageDialog(null, "Suma: " + (l1 + l2));
      }
    }
  }

}

Zobacz demo działania programu:


Podobnie, stosując metodę double Double.parseDouble(String s) możemy przekształcać znakową reprezentację liczb rzeczywistych w ich postać binarną.

Skaner możemy łączyć z dialogami wejściowymi. Może on bowiem pobierać dane z różnych źródeł m.in. z napisów (obiektów klasy String).
Ilustruje to poniższy program - odpowiednik poprzedniego sumowania.

import java.util.*;
import javax.swing.*;

public class ScanNumsFromString {

  public static void main(String[] args) {
    String in = JOptionPane.showInputDialog("Podaj dwie liczby całkoitew rozdzielone spacjami");
    if (in != null) {
      Scanner scan = new Scanner(in);
      if (scan.hasNextInt()) {
        int n1 = scan.nextInt();
        if (scan.hasNextInt()) {
          int n2 = scan.nextInt();
          JOptionPane.showMessageDialog(null, "Suma: "+  (n1 + n2));
        }
      }
    }
  }
}
Tutaj jako źródło danych, przy wywołaniu konstruktora skanera ustalamy napis reprezentowany przez zmienną in (wynik wprowadzenia informacji w  dialogu wejściowym). Metoda hasNextInt() zwraca true, jeżeli w źródle danych jest jeszcze "nie zeskanowana" liczba całkowita. Napis  może być pusty, lub zawierać symbole nie dające się zinterpretować jako liczba całkowita. Wtedy metoda hasNextInt() zwróci false i nie będziemy wykonywać żadnych operacji.
Skaner dysponuje wieloma innymi (bogatymi) możliwościami interpretacji i skanowania danych źródłowych. Będzie o nich mowa później.

4. O metodach equals() i toString()

Skoro operator == użyty wobec referencji porównuje tożsamość obiektu (czy referencje wskazują na ten sam obiekt), to jak porównać zawartość obiektów (czy obiekty wskazywane przez referencje reprezentują identyczne dane?).
Otóż to zależy od definicji klasy.

Do porównywania zawartości (treści) obiektów służy metoda:
  
     public boolean equals(Object o)

która zwraca wartość true, jeżeli ten obiekt, na rzecz którego wywołano metodę ma taką samą zawartość jak obiekt, przekazany jako argument.

W większości standardowych klas Javy (takich jak np. String) metoda equals() jest odpowiednio zdefiniowana. W naszych własnych klasach musimy sami dostarczyć jej definicji.
W klasie Para może ona wyglądać następująco:
public class Para {
 private int a, b;
 // ...
 public boolean equals(Object obj) {
   if (this == obj) return true;                   // 1
   if (obj == null) return false;                  // 2
   if (getClass() != obj.getClass()) return false; // 3 
   Para other = (Para) obj;                        // 4 
   if (a != other.a || b != other.b) return false; // 5
   return true;                                    // 6
 }
// ...
}
Znaczenie  poszczególnych wierszy kodu (wg numerów podanych w komentarzach) jest następujące:
  1. Jeżeli to jest ten sam obiekt - zwracamy true (para jest równa samej sobie).
  2. Jeżeli jako argument przekazano null - pary nie są takie same (null nie jest parą).
  3. Jeżeli klasy tego obiektu i argumentu są różne - pary nie są takie same (argument nie jest referencją do obiektu klasy Para); proszę zwrócić uwagę, że argument jest formalnie typu Object, więc można tu podać referencję do obiektu dowolnej klasy; metoda getClass() pozwala uzyskać klasę obiektu i w ten sposób możemy sprawdzić czy obiekt wskazywany przez argument należy do tej samej klasy co TEN obiekt (czyli klasy Para).
  4. Po to, by móc odwoływać się do składowych pary-argumentu musieliśmy dokonać rzutowania (konwersji zawężającej) do klasy Para.
  5. Jeżeli którykolwiek ze składników pary w tym obiekcie nie jest taki sam jak odpowiedni składnik pary w obiekcie-argumencie - pary nie są takie same.
  6. W przeciwnym razie (oba składniki par są odpowiednio takie same) zwracamy true (pary są takie same)
Poniższy program testowy pokazuje wyniki porównań:
public class Test {
  
  static void test(String jakiePary, Para p1, Para p2) {
    System.out.println(jakiePary + "   ==   - daje wynik: " + (p1 == p2));
    System.out.println(jakiePary + " equals - daje wynik: " + p1.equals(p2));
  }

  public static void main(String[] args) {
    
    Para para1 = new Para(1,2);
    Para para2 = new Para(3,4);
    Para para3 = new Para(1,2);
    test("para1 i para2", para1, para2);
    test("para1 i para3", para1, para3);
    para2 = para1;
    System.out.println("Po podstawieniu para2 = para1");
    test("para1 i para2", para1, para2);
    test("para1 i para3", para1, para3);
  }

}
Wydruk:
para1 i para2   ==   - daje wynik: false
para1 i para2 equals - daje wynik: false
para1 i para3   ==   - daje wynik: false
para1 i para3 equals - daje wynik: true
Po podstawieniu para2 = para1
para1 i para2   ==   - daje wynik: true
para1 i para2 equals - daje wynik: true
para1 i para3   ==   - daje wynik: false
para1 i para3 equals - daje wynik: true

Do porównywania zawartości obiektów na równość - nierówność nie wolno stosować operatora ==.
Należy zawsze stosować metodę equals(...), o ile jest zdefiniowana w klasie obiektów.

Dodatkowe uwagi:

Szczególną rolę w definicji klas pełni metoda public String toString().

Metoda:

                public String toString()

służy do przedstawiania zawartości obiektów w postaci napisów.
Powinna zwracać napis, reprezentujący zawartość obiektu.
W naszych klasach sami decydujemy o tym, jak ma wyglądać ten napis.

Metoda toString() jest ważna, bowiem jest wykorzystywana w wielu standardowych klasach Javy do pokazywania napisowej reprezentacji obiektu. W szczególności, jeżeli metodzie println(...) przekażemy jako argument referencję do obiektu, to zostanie automatycznie wywołana metoda toString() i jej wynik wyprowadzony na konsolę.

Możemy teraz w klasie Para zrezygnować z metody show() (wypisującej zawartośc pary na konsolę) i zastąpić ją metodą toString(). Kod będzie bardziej naturalny.
public class Para {

 private int a;
 private int b;
 
 // ...
 
 public String toString() {
   return " ( " + a + " , " + b + " )";
 }
 
 public static void main(String[] args) {
   Para p1 = new Para(1,2);
   Para p2 = new Para(3,4);
   System.out.println(p1);
   System.out.println(p2);
   System.out.println(p1.add(p2));
 }

}
wypisze na konsoli:

 ( 1 , 2 )
 ( 3 , 4 )
 ( 4 , 6 )

Co równie  ważne, w klasach o uniwersalnym przeznaczeniu (takich jak klasa Para) nie powinniśmy przesądzać o różnych szczegółach ich użycia (np. czy informacja o składnikach pary ma być wypisywana na konsoli czy może pokazywana w jakimś dialogu).  Dlatego należało zrezygnować z metody show() i wprowadzić metodę toString(). Uzyskany od niej wynik (zawartość pary) może być pokazywany  dowolny sposób.


5. Wyliczenia (enumeracje)

Ogólnie, wyliczenie to nic innego jak konkretny zbiór nazwanych stałych.
Posługując się tylko typem int możemy np. wprowadzić coś w rodzaju wyliczenia pór roku.

final int ZIMA = 0, WIOSNA = 1, LATO = 2, JESIEN = 3;

i używać ich w programie:

int poraRoku = LATO;

Takie podejście ma jednak wady:
W Javie usunięto te wady, pozostawiając jednocześnie zaletę efektywności działania (tak jak na liczbach całkowitych), wprowadzając specjalny rodzaj typów referencyjnych oznaczanych słowem kluczowym enum.

Definicja typu wyliczeniowego polega na umieszczeniu po słowie enum w nawiasach klamrowych elementów wyliczenia, rozdzielonych przecinkami:

[ public] enum NazwaTypu {
    elt1, elt2, ..., eltN
}

gdzie elt - elementy wyliczenia

Np.

enum Pora { ZIMA, WIOSNA, LATO, JESIEŃ }

Tutaj Pora jest nazwą typu wyliczeniowego, a ZIMA, WIOSNA, LATO,  JESIEŃ oznaczają stałe tego typu (zbiór nazwanych stałych).

Zmienna zadeklarowana jako:

Pora p;

będzie mogła przyjmowac wartości: Pora.ZIMA, Pora.WIOSNA, Pora.LATO, Pora.JESIEŃ (oraz null, ponieważ jest typu referencyjnego). Zauważmy, że wartości te to stałe statyczne i są one typu refrencyjnego Pora (czyli ani liczby ani napisy).

Wobec danych typów wyliczeniowych możemy stosować różne  metody (obiektowość! - są zdefiniowane operacje). Jak to możliwe i jak można rozszerzać funkcjonalność typów wyliczeniowych dowiemy się w części poświęconej programowaniu obiektowemu.
Teraz wystarczy wiedza całkiem praktyczna. M.in.:

Przykładowy program pozwala na podanie nazwy pory roku i opisuje ją w okienku komunikatów. 
import javax.swing.*;

public class PoryRoku {
  
  enum Pora { ZIMA, WIOSNA, LATO, JESIEŃ };
  
  public static String opisz(Pora p) {
    
    if (p == Pora.ZIMA) return p + " - pada śnieg.";
    if (p == Pora.WIOSNA) return p + " - kwitnie bez.";
    if (p == Pora.LATO) return p + " - jest ciepło.";
    if (p == Pora.JESIEŃ) return p + " - pada deszcz.";
    return "Pora roku nie została wybrana";
  }
  
  public static void main(String[] args) {
    
    String nazwa = JOptionPane.showInputDialog("Podaj porę roku:");
    Pora p = Pora.valueOf(nazwa);
    JOptionPane.showMessageDialog(null, opisz(p) );
  }

}

Zobacz demonstrację działania programu



Np. po podaniu w dialogu wejściowym napisu "LATO" uzyskamy następujący wynik:

r