Typy danych. Operatory i wyrażenia


1. Literały i zmienne

Działanie każdego programu polega na przetwarzaniu danych.
Dane w programie przedstawiamy za pomocą literałów i zmiennych oraz stałych.

Literał - to napis w programie reprezentujący w sposób bezpośredni wartość danej


Na przykład napis:
111
reprezentuje liczbę 111 i jest traktowany jako liczba 111.

Literały nie wystarczą, aby zapisać w programie algorytm. Spróbujmy np. napisać fragment programu, który przedstawia następującą sytuację: na koncie bankowym jest 1000 zł, wpłacamy na konto 100 zł, wypłacamy 50 i następnie znowu wpłacamy 200. Za każdym razem należy wyprowadzić stan konta:
Mając do dyspozycji tylko literały musielibyśmy pisac tak:
System.out.println(1000 + 100);
System.out.println(1000 + 100 - 50);
System.out.println(1000 + 100 - 50 + 200);
Jest to oczywiście bez sensu. Musimy jakoś zapamiętywać bieżący stan konta. Do przechowywania danych w programie i ponownego ich użycia w kolejnych operacjach służą zmienne.

Zmienna jest symbolem w programie, oznaczającym obszar w pamięci komputera, w którym mogą być zapisywane różne dane.
Czyli:

Nasz poprzedni przykład z kontem używając zmiennej skonta (przechowującej stan konta) możemy zapisać tak:
// ...
skonta = 1000; // zmienna skonta reprezentuje aktualny stan konta; poczatkowo 1000

skonta = skonta + 100; // po wpłacie stan konta ulega zmianie; teraz zmienna skonta ma wartość 1100
// ...
skonta = skonta - 50; // a teraz 1050 (po wypłacie)
// ...
skonta = skonta + 200; // po ostatniej wpłacie stan konta wynosi 1250
// ...
System.out.println(konto); // możemy go wypisać na konsoli, wyprowadzając wartość zmiennej skonta

Istnieje również specjalny rodzaj zmiennych, takich, których wartość (po początkowym ustaleniu) nie może się zmieniać. Nazywane są one stałymi.


2. Pojęcie typu. Typy proste

I znowu: działanie każdego programu polega na przetwarzaniu danych. A dane są zapisywane w pamięci komputera. Aby kompilator mógł właściwie stworzyć odpowiedni kod binarny, który m.in. specyfikuje w jaki sposób dane są zapisywane w pamięci, ile zajmują miejsca, w jaki sposób są wykonywane operacje na danych, każdej jednostce danych trzeba przypisać typ.

Typ danej - to zbiór jej możliwych wartości plus zestaw operacji, które można nad nimi wykonywać.
Jednocześnie określa on rozmiar pamięci, potrzebny do przechowania danej oraz sposób zapisu danej w pamięci komputera

Np. dane typu całkowitoliczbowego mogą przybierać wartości całkowitoliczbowe z jakiegoś przedziału, który zależy od tego ile miejsca w pamięci zajmują dane tego typu oraz jak są w pamięci zapisywane. Określone są też operacje, które możemy wykonywać na tych danych: np. dodawanie, mnożenie, dzielenie, reszta z dzielenia oraz sposób w jaki te operacje są wykonywane.

Szczególnym sposobem wyrażenia opracji na danych są operatory języka.

Operator - to specjalny symbol języka służący do przeprowadzania operacji na danych

Skoro zmienne i literały reprezentują dane w programie, to typy dotyczą wartości zmiennych, stałych i literałów.
Mówimy: zmienna typu ..., stała typu ... literał typu ...
Ale nie tylko.
Ze zmiennych, stałych, literałów i operacji na nich (definiowanych m.in. przez operatory) budowane są wyrażenia. Każde wyrażenie jest obliczane (inaczej powiemy: opracowywane) i ma jakiś wynik. Wyniki wyrażeń  (powiemy krótko: wyrażenia) też mają określony typ.

Typy mają swoje nazwy.
W języku istnieje szereg tzw. typów prostych, których nazwy stanowią zarezerwowane słowa kluczowe języka.

Słowa kluczowe języka - to słowa, które mają specjalne znaczenie (np. oznaczają instrukcje sterujące lub nazwy typów prostych).
Słowa kluczowe mogą być zarezerwowane i wtedy nie mogą być używane w innych kontekstach poza znaczeniem opisanym przez składnię języka.
Typy proste  przedstawia poniższa tablica.


Typy proste

nazwa typu

liczba bajtów

dopuszczalne wartości

znaczenie

byte

1

od –128 do 127

liczby całkowite

short

2

od –32768 do 32767

int

4

od –2147483648

do 2147483647

long

8

od –9223372036854775808

do 9223372036854775807

float

4

od –3.4028234663852886E38

do -1.401298464324817E-45

i

od 1.401298464324817E-45

do 3.4028234663852886E38

liczby rzeczywiste

double

8

od -1.7976931348623157E308

do -4.9E-324

i

od 4.9E-324

do 1.7976931348623157E308

char

2

od 0 do 65556

znaki Unicodu

boolean

1

true, false

wartości logiczne: prawda, fałsz


Typy byte, short, int, long, float, double i char nazywane są typami liczbowymi lub numerycznymi, ponieważ dane tych typów reprezentują liczby.

W szczególności znaki (typ char) reprezentowane są przez ich liczbowe, nieujemne całkowite, kody (zatem są nieujemnymi liczbami całkowitymi). Istnieją różne systemy kodowania znaków, np. kod ASCII zawiera 256 kodów znaków (liczby od 0 do 255). W Javie używany jest tzw. Unicode, który okresla 65557 kodów znaków (nie wszystkie sa jeszcze zdefiniowane), dzięki czemu można przedstawić znaki niemal wszystkich języków (np. uproszczonego chińskiego).

Przy okazji przedstawiania typów kilka słów powiemy o niektórych operacjach w których mogą brać udziała dane tych typow.

Wartości typów liczbowych są używane w operacjach arytmetycznych, a wartości typu char dodatkowo stanowią elementy łańcuchów znakowych (napisów).

Operacje arytmetyczne, zapisywane za pomocą operatorów, to:
W przypadku liczb całkowitych operacja dzielenie jest dzieleniem całkowitoliczbowym (a więc zwraca wynik dzielenia po odrzuceniu części ułamkowej np. 1/3 „tak naprawdę” równe jest jedna trzecia, ale wynikiem tego wyrażenia będzie 0, gdyż biorą w nim udział dwie liczby całkowite i mamy tu dzielenie całkowitoliczbowe).

Na wartościach typów numerycznych możemy również wykonywać operacje relacyjne (porównania: <, >, <=, >=, == (czy równe?), != (czy nie równe?) i bitowe.

Typ boolean (nazywany typem logicznym, orzecznikowym, boolowskim) służy do przedstawiania wartości logicznej o znaczeniu prawda lub fałsz. Wartości tego typu są używane w wyrażeniach logicznych, są też ich wynikiem, jak również są wynikiem wyrażeń relacyjnych. Mogą być również wykorzystywane bezpośrednio w instrukcjach sterujących (jako warunki).
Np. wyrażenie a > b będzie miało wartośc typu boolean stwierdzającą czy wynik porównania jest prawdziwy czy fałszywy. Wyrażenie logiczne " x i y ", zapisywane w Javie jako x && y będzie składniowo poprawne tylko wtedy, gdy x oraz y są typu boolean, a jego wartośc będzie wartością typu boolean, stwierdzającą czy predykat "x i y" jest prawdziwy czy nie.

Wszystkie inne dane (poza liczbami - w tym znakami - i wartościami logicznymi) reprezentują w Javie  obiekty, których atrybuty (a zatem możliwe "wartości" danych) oraz funkcjonalność (możliwe operacje na danych) definiują określone klasy.
Zatem  nazwę klasy można w tym przypadku traktować jako nazwę typu danego obiektu.

W standardowym zestawie klas Javy występuje np. klasa String. Definiuje ona wspólne własciwości i funkcjonalność obiektów, będących łańcuchami znakowymi (napisami).
Np. każdy łańcuch znakowy ma długość, niepuste łańcuchy znakowe mają znaki, obiekty klasy String są obowiązane "świadczyć usługi" polegające np. na odpowiedzi na nasze pytania: jaka jest długość łańcucha? czy łańcuch zawiera podany ciąg znaków? itp. - co możemy traktować jako operacje na łańcuchach znakowych.

Typy takie ogólnie nazywają się typami referencyjnymi (skąd taka nazwa - zobaczymy w dalszej części tekstu).
Szczególnym rodzajem typów referencyjnych są typy wyliczeniowe,  wprowadzana za pomocą słowa kluczowego enum.  Będzie o nich mowa w dalszej części tekstu.



3. Typy i użycie literałów

Mamy do dyspozycji:  literały liczbowe, literały znakowe i literały łańcuchowe.

Literał liczbowy to bezpośredni zapis konkretnej liczby.
Uwaga. Zapisując liczby rzeczywiste w programach stosujemy jako separator miejsc dziesiętnych kropkę (a nie przecinek). 
Na przykład:

101.3



Literał znakowy
okresla jeden bezpośrednio zapisany w programie znak. Do zapisania znaku będziemy stosować apostrofy.
Na przykład: 'a'  'b' 'z' '5'

Literały łańcuchowe lub napisowe - to bezpośrednio zapisane w programie ciągi znaków (napisy, inaczej zwane też łańcuchami znakowymi), które chcemy traktować jako teksty, a nie elementy języka. Ciągi takie ujmujemy w cudzysłów.
Na przykład:

"ala ma kota"
"if"

Przy okazji: dlaczego musimy zapisywać znaki i ciągi znaków w apostrofach/cudzysłowie?
Otóż nieujęte w cudzysłów napisy i nieujęte w apostrof znaki alfabetyczne traktowane są jako nazwy zmiennych lub słowa kluczowe (oznaczające instrukcje lub inne elementy języka). Np.
a = b;
będzie traktowane jako przypisanie zmienej a wartości zmiennej b. Jeśli chcemy przypisać zmiennej a znak b, musimy napisać:
a = 'b';

a taki zapis:
x = if;
będzie potraktowany jako błąd składniowy, gdyż if jest w Javie  słowem zarezerwowanym .



O typy literałów troszczy się sam kompilator, stosując przy tym pewne reguły, mianowicie: W niektórych przypadkach programista może zmienić interpretację literałów liczbowych, stosując specjalne modyfikatory, mianowicie:
Liczby całkowite mogą być zapisywane:
Liczby rzeczywisty mogą być zapisywane w notacji naukowej (inżynierskiej) z wykorzystaniem litery e lub E np. 2e+9 oznacza 2 pomnożone przez 10 do potęgi 9, 3e-11 oznacza 3 pomnożone przez 10 do potęgi -11.

Po co są te wszystkie dodatkowe napisy (przyrostki, modyfikatory) ? Sprawa jest oczywista w przypadku zmiany podstawy systemu liczbowego. Literał 10 jest traktowany jako zapis w sytemie dziesiętnym ( i ma wartość 10). Jezeli chcemy "dać znać", że chodzi nam o liczbę 10 zapisaną w systemie szesnastkowym (a jej wartośc dziesiętnie jest już zupelnie inna - 16), to musimy użyć jakiejś konwencji (w przypadku Javy przedrostka 0x lub 0X).
Ale modyfikatory L czy F - po co?
Otóż, niestety kompilator Javy przypisując typ literałom liczbowym nie kieruje się wartością zapisanych literalnie liczb. Jak powiedziano - wszystkie literały całkowitoliczbowe traktuje jako typu int, a rzeczywiste (czyli z kropką dziesiętną lub zapisane w notacji naukowej) - jako double.
Zwróćmy jednak uwagę: dane typu int zajmują tylko 4 bajty w pamięci i największa liczba, którą można zapisać w tych czterech bajtach - to 2147483647 . Jeśli zatem zapiszemy w programie literał 2147483649, to będzie on potraktowany jako liczba typu int, tylko niestety nie zmieści się ona w obszarze przeznaczonym na przechowywanie takich liczb. Kompilator rozpozna tę sytuację i zgłosi błąd. Powinniśmy zatem zapisać literał jako 2147483649L.

Gorszy przypadek: jeśli zapiszemy operację dodawania dwóch literałów 2147483647 + 10, to obie liczby będą typu int (i faktycznie zmieszczą się w obszarach 4 bajtowych). Kompilator nie potrafi przeprowadzić sumowania, wie tylko, że dodawane są dwie liczby typu int i zakłada, że wynik będzie też typu int. Zatem wydziela "pod wynik" tego wyrażenia obszar 4 bajtowy. A w 4 bajtach wynik się nie mieści i dostaniemy nieprawidłowy, bardzo dziwny, rezultat (bez żadnego błędu w kompilacji ani w fazie wykonania). Aby tego uniknąć znowu musimy podac przyrostek L.

A co zrobić z wyrażeniem 10/3 - obie liczby są traktowano jako typu int, zatem przeprowadzone będzie dzielenie całkowitoliczbowe, które da w wyniku 3 (a nie 3.33333...). Jesli chodzi nam o operację na liczbach rzeczywistych, to musimy albo użyć przyrostka d (wskazując, że chodzi nam o literał typu double, albo podać explicite kropkę dziesiętne w zapisie którejś z liczb np. 10. lub 10.0)

Omówione przypadki pokazuje testowy program.

public class LitLicz {

 public static void main(String[] args) {

 System.out.println( 10 + 0x10 );
 System.out.println( 10/3 );
 System.out.println( 10./3 );
 System.out.println( 10d/3);
 System.out.println( 2147483648L );
 System.out.println( 2147483647 + 1 );
 System.out.println( 2147483647L + 1 );

 }
}
Zobacz demonstrację działania programu.
Wynik działania programu pokazuje poniższy listing.
26
3
3.3333333333333335
3.3333333333333335
2147483648
-2147483648
2147483648

Zanotujmy (do pełniejszego wyjaśnienia w przyszłości) dwie obserwacje:


Literały znakowe (typ char) zapisujemy jako pojedyncze znaki w apostrofach np. 'a' , '+' itp., a literały łańcuchowe (napisy) - jako ciagi znaków w cudzysłowie (np. "ala ma kota").

Literały łańcuchowe oznaczają obiekty klasy String.


Użycie lewego ukośnika (backslash) - nazywanego też symbolem ucieczki (escape character) - pozwala na zapisywanie w literałach znakowych i łańcuchowych specjalnych znaków.
Uwaga: ze względu na wczesne opracowywanie literałów niedopuszczalne jest stosowanie bezpośrednich kodów Unicode dla znaków LF (\u000a) i CR (\u000d), gdyż spowoduje to podział kodu programu, a nie wytworzenie literałow, odpowiadających znakom LF i CR. Zamiast tego należy stosowac znaki \n i \r.

Znaki specjalne
Zapis
Przejście do nowego wiersza (Line feed - LF)\n
Tabulacja (Tab)\t
Backspace (BS)\b
Powrót karetki (carriage return - CR)\r
Nowa strona (form feed - FF)\f
Apostrof\'
Cudzysłów\"
Lewy ukośnik (backslash) \\
Dowolny znak o kodzie NNNN (gdzie N - cyfra szesnastkowa) w Unicodzie

\uNNNN

Należy zawsze pamiętać, że dwu- lub 6-znakowa (w tekście programu) sekwencja zapisana w apostrofach (lub wewnątrz zestawu znaków ujętych w cudzysłowy) i zaczynająca się od ukośnika tak naprawdę (czyli tak jak ją widzi kompilator) jest jednym znakiem.

Uwaga:
Niektóre znaki specjalne są przydatne przy wyprowadzaniu informacji, a ich dziwnie brzmiące czasem nazwy związane są z czasami gdy jedynym urządzeniem wyjściowym była drukarka (np. powrót karetki to przesunięcie głowicy drukarki na pierwszą pozycję w wierszu - jeśli urządzeniem wyjściowym jest konsola, to wyprowadzenie tego znaku powoduje przejście kursora do pierwszzej pozycji linii ekranu, w której aktualnie się znajduje).

A skąd na tej liście cudzysłów i apostrof poprzedzone ukosnikiem? Odpowiedź jest prosta. Skoro znaki ujmujemy w apostrofy, a łańcuchy znakowe w cudzysłowy, to jak zapisać znak apostrofu lub cudzysłów ? Do tego właśnie służy ukośnik - piszemy więc '\'' i '\"'.
W końcu - czasem będziemy chcieli podać literalnie znak \. Trzeba go wtedy wpisać podwójnie.

Poniższy program testujący pokazuje kilka przykładów użycia literałów znakowych i łańcuchowych. Przy okazji poznamy też inną metodę wyprowadzania danych na konsolę: o ile System.out.println(...) wyprowadza podane jako argument dane w bieżącym wierszu konsoli z następującym potem przejściem do nowego wiersza, to System.out.print(...) - robi to samo, ale bez przejścia do nowego wiersza.

public class LitZn {

 public static void main(String[] args) {
 System.out.print("\u03b1\u03b2\u03b3 ");
 System.out.print('\\');
 System.out.print('\'');
 System.out.print(" alfa beta gamma '");
 System.out.println("\nalfa\nbeta\ngamma");
 System.out.println("c:\\util\\bak");
 }
}
Zobacz demonstrację działania programu.

Wydruk działania programu przedstawiono poniżej.
αβγ \' alfa beta gamma '
alfa
beta
gamma
c:\util\bak

Zwróćmy uwagę:



4. Typy zmiennych. Deklaracje

W przeciwieństwie do literałów, typy zmiennych i stałych programista musi podać sam w deklaracjach.

Deklaracja zmiennej (stałej) polega na określeniu nazwy zmiennej (stałej), jej typu oraz ew. pewnych innych właściwości, które są istotne wtedy, gdy zmienna jest polem klasy (reprezentuje atrybut obiektów tej klasy)



Składnię deklaracji zmiennej - w uproszczeniu - można przedstawić w następujący sposób:

nazwa_typu nazwa_zmiennej;

Na przykład:

int a;              // deklaracja zmiennej całkowitotoliczbowej a
char c;            // deklaracja zmiennej c, która może zawierać znaki Unikodu
double price;   // deklaracja zmiennej typu double o nazwie price

Przy okazji deklarowania zmiennych można ustalać ich wartości, co nazywa się inicjacją.

Inicjacja zmiennej - to ustalenie jej wartości przy okazji deklaracji


Deklarację z inicjację zapisujemy w formie:

nazwa_typu nazwa_zmiennej = wyrażenie;


Na przyklad:

int a = 3;

co jest skróconą formą od:

int a;
a = 3;

W tym przypadku wyrażeniem inicjującym był literał. Ogólnie może być to dowolne wyrażenie.

Np.
int a = 3;
int b = a + 1; // deklaracja zmiennej b i ustalenie wartości na 4

Zauważmy, że użycie zmiennej a w wyrażeniu inicjującym wartośc zmiennej b było możliwe tylko dlatego, że deklaracja zmiennej a wystąpiła przed użyciem jej w tym wyrażeniu.

Do deklarowania stałych używamy słowa kluczowego final.

Wartość stałej można ustalić tylko raz i poźniej ta wartość nie może być już zmieniona


Piszemy:

final nazwa_typu nazwa_stałej [ = wyrażenie ]

przy czym inicjacja (podana w nawiasach kwadratowcyh) jest tu opcjonalna, bowiem istotne jest tylko to by wartość stałej została ustalona tylko raz. Najczęściej jednak wartość tę ustalać będziemy w momencie deklaracji.

Np.

final int NUM = 55;
final double PI_SMALL = 3.14;
final char C;
...
C = 'c';


Deklaracje zmiennych i stałych, które oznaczają obiekty zapisujemy w analogiczny sposób.
Zobaczmy.
Jeżeli int ma być typem zmiennej x (która oznacza liczbę całkowitą) to piszemy:

int x;

Podobnie jeżeli s jest nazwą zmiennej, która oznacza obiekt klasy String, to piszemy:

String s;

To samo ze zmiennymi, które oznaczają obiekty każdej innej klasy (np. z zestawu "klas standardowych", dostarczanych wraz ze środowiskiem Java 2 SDK lub też klas, które sami zdefiniujemy):

JButton b;      // zmienna b  oznacza obiekt klasy JButton
JFrame frame; // zmienna frame  oznacza obiekt klasy JFrame

Oczywiście, zmienne, oznaczające obiekty również można inicjować w czasie deklaracji.
Zobaczymy to na przykładach dalej (bowiem wymaga to dodatkowej informacji o tworzeniu obiektów), ale już teraz można podać prostą postać inicjacji  napisów:

String s = "Ala ma kota";   // zmienna s nędzie oznaczać napis "Ala ma kota".



Reguły dobrego stylu programowania w zasadzie nakazują umieszczanie deklaracji każdej zmiennej w odrębnym wierszu programu wraz z dodatkowym komentarzem opisującym przeznaczenie zmienenej, gdy nazwa zmiennej nie mówi całkowicie wyraźnie co dana zmienna oznacza
Zauważmy jeszcze, że dopuszczalne jest deklarowanie kilku zmiennych tego samego typu w jednej deklaracji poprzez wymienienie ich (wraz z ew. inicjatorami) na liście rozdzielonej przecinkami np.

int num = 1, count, ff = 10;
JButton b1, b2, b3;



Trzeba koniecznie pamętać, że wszystkie zmienne w programie muszą być zadeklarowane.

Deklaracje obowiązkowo muszą poprzedzać użycie zmiennych i stałych w innych instrukcjach programu.
Nie znaczy to jednak, że deklaracje muszą poprzedzać wszystkie instrukcje programu.
Wręcz przeciwnie: zmienne i stałe deklarujemy zwykle możliwie najbliżej miejsca ich użycia, co zwiększa czytelność kodu.


Zobaczmy przykładowy program, obliczający cenę komputera na podstawie cen jego składowych:
public class CompPrice {

 public static void main(String[] args) {

 int cProc = 700; // cena procesora
 int cPly = 500; // ... płyty
 int cPam = 300; // innych składnikow ...
 int cDysk = 400;
 int cInn = 500;

 final double VAT = 1.22; // narzut podatku VAT

 // Liczymy cenę komputera bez monitora
 // wyrażenie sumujące składniki zapisując
 // jako wyrażenie inicjującę zmienną cKomp

 double cKomp = (cProc + cPly + cPam + cDysk + cInn) * VAT;

 System.out.println("Cena komputera bez monitora wynosi :");
 System.out.println(cKomp);

 int cMon = 1100; // cena monitora netto
 cKomp = cKomp + cMon * VAT; // i nowa cena komputera z monitorem

 System.out.println("Cena komputera z monitorem wynosi :");
 System.out.println(cKomp);

 }
}


Zauważmy: gdybyśmy zapomnieli dodać słówko int przed cMon - zmienna cMon byłaby niezadeklarowana.
 cMon = 1100; // cena monitora netto
 cKomp = cKomp + cMon * VAT; // i nowa cena komoutera z monitorem
Kompilator wykryje tę sytuację i poinformuje nas o błędzie

CompPrice.java:19: cannot resolve symbol
symbol : variable cMon
location: class CompPrice
cMon = 1100; // cena monitora netto
^
CompPrice.java:20: cannot resolve symbol
symbol : variable cMon
location: class CompPrice
cKomp = cKomp + cMon * VAT; // i nowa cena komputera z monitorem
^
2 errors
Uwaga: w niektórych środowiskach uruchomieniowych kompilacja jest inkremetalna (w trakcie pisania programu) i wiersze, zawierające błędy, są od razu oznaczane jako błędne.

Zobacz multimedialną demonstrację tej sytuacji w środowisku Eclipse.


Na marginesie, z komunikatem "cannot resolve symbol" będziemy się spotykać dość często. Oznacza on (zwykle), że używamy jakiejś nazwy, której kompilator nie może zidentyfikowac (np. niezadeklarowanej zmiennej).
Java rozróżnia wielkie i male litery w nazwach zmiennych, stałych, metod, klas
Jednym z typowych źródeł takich błędów są błędy w pisowni, a szczególnie mylenie wielkich i małych liter w nazwach. O ile z własnymi programami zwykle nie będziemy mieli tutaj wielkich problemow (ostatecznie sami wymyśliliśmy nazwy), to nazwy stosowane w standardowych pakietach Javy mogą nam sprawiać więcej kłopotu.
Stosowana jest tutaj dość klarowna konwencja nazewnicza, która bardzo nam pomoże, ale na początku często nie będziemy jeszcze pewni jak pisać np. string czy String,  HashTable czy Hashtable?


Identyfikatory

Nazwy zmiennych, stałych, metod, klas - nazywają się identyfikatorami.
Identyfikatory muszą zaczynać się od litery lub podkreślenia (znaku _) i mogą składać się z dowolnego ciągu znaków alfanumerycznych (liter i cyfr) i/lub znaków podkreśleń.
Jako identyfikatorów nie wolno używać zarezerwowanych słów języka .

Konwencje nazewnicze:

Uwaga: konwencja wyróżniania składników nazwy poprzez rozpoczynanie ich dużą literą nazywa się notacją węgierską



Na zakończenie warto jeszcze raz zastanowić się jaki jest sens pojęcia typu.
Na "niskim poziomie" (instrukcji maszynowych) każda jednostaka danych rzeczywiście musi mieć typ. Wynika to ze znanego już nam faktu, że każda informacja (instrukcje maszynowe, dane) jest przedstawiana w pamięci komputera za pomocą ciągów bitów o wartościach 1 lub 0. Nazwa zmiennej oznacza zaś obszar pamięci, w którym znajduje się oznaczana przez tę zmienną wartość.
Np. jeśli mamy zmienne x i y, to nazwy te (jakoś) odsyłają do początków obszarów, w których zapisane są jakieś ciągi bitowe:

x 1 1 0 0 1 0 1 1 ....
y 1 ... 0 1 1 0 0 1 0 0 1 ...


Jak w takiej sytuacji komputer ma zinterpretować wyrażenie x + y ? Gdzie kończą się wartości x i y ? Co oznacza operacja + ? Odpowiedzi na te pytania wynikają właśnie z typów zmiennych x i y, i dlatego typy te muszą być określone.

Przykładowo: jeśli zmienne x i y są typu int, to do przechowywania wartości tych zmiennych zostaną wydzielone obszary pamięci o określonej długości (4 bajty), a instrukcja dodawania będzie skutkować w wygenerowaniu odpowiednich instrukcji maszynowych, które w sumie umożliwią dodanie do siebie dwóch wartości z tych obszarów przy zastosowaniu arytmetyki stałopozycyjnej. Gdyby zmienne x i y były typu float, to obszary zajmowanej przez nie pamięci byłyby inne (8 bajtów), a dodawanie realizowane w arytmetyce zmiennopozycyjnej.

Dlaczego jednak w programie pisanym w Javie, która przecież jest językiem wysokiego poziomu (abstrahującym - choć jak widzimy nie do końca - od "szczegółow technicznych") programista zawczasu musi deklarować zmienne, określając przy tym ich typy?

Są języki, które tego nie wymagają. Programuje się w nich nieco łatwiej, bo programista nie zaprząta sobie głowy szczegółami technicznymi i ma mniej pisania.

Ale są argumenty, które przemawiają za określaniem typów zmiennych przez programistę. Warto przede wszystkim zauważyć, że dotyczy to szczególnie typów numerycznych. Dane różnych typów numerycznych (jak widzieliśmy) zajmują różną objętość pamięci. Mając do wyboru kilka typów liczbowych programista może więc lepiej sterować zajętością pamięci i efektywnością programu.
Jeśli np. musi przechowywać w pamięci 10 tys. liczb i wie, że wszystkie one mogą być tylko całkowite, a ich wielkości mieszczą się 4 bajtach, to używając typu int zamiast typu double oszczędza 40 tys. bajtów oraz znacznie zwiększa efektywność programu, gdyż działania na liczbach całkowitych wykonują się zazwyczaj wielokrotnie szybciej niż na liczbach rzeczywistych.
Powiedzmy jednak szczerze: ten kontekst jest bardzo ważny w zastosowaniach naukowych, przy dużych obliczeniach itd., natomiast na skutek postępu w sprzęcie oszczędności rzędu 1 MB pamięci czy kilka milionów operacji w programie dla codziennych powszechnych zastosowań oprogramowania nie są już tak istotne.

Dochodzimy więc do chyba najbardziej właściwej odpowiedzi na pytanie o sens deklarowania zmiennych i określania ich typów.
Otóż określanie typów danych (nawet tych najbardziej elementarnych) w deklaracjach umożliwia kompilatorowi kontrolę poprawności programu np. stwierdzenie czy zmienne określonych typów stosujemy we właściwych kontekstach albo czy przekazujemy funkcjom (metodom) argumenty właściwych typów. Nazywa się to statyczną kontrolą zgodności typów (przez kompilator) i często pomaga wykrywać błędy w programie już w fazie kompilacji.

Java jest językiem ze   statyczną i ścisłą kontrolą zgodności typów i temu głównie służy wybrany przez twórców Javy sposób kompilowania programów, skutkujący w składni języka.

Statyczna ścisła kontrola zgodności typów oznacza, że typ wyniku każdego wyrażenia znany jest już w fazie kompilacji


Przeświadczenie o tym, że statyczna ścisła kontrola zgodności typów chroni przed błędami, żywione przez twórców Javy, chyba najbardziej zaważyło na tym, iż w Javie trzeba deklarować zmienne i w deklaracjach określać ich typy, a stosując literały trzeba pamiętać o konwencjach traktowania literalnie podanych danych.

5. Operatory i wyrażenia

5.1. Przegląd

Ze zmiennych, stałych, literałów oraz wywołań metod (funkcji), posługując się operatorami języka i nawiasami możemy konstruować wyrażenia.

Jesli a i b są nazwami zmiennych typu numerycznego, to wyrażeniami będą np.:
a
1
b + 1
a * (b + 10)
'a'
"ala ma kota"

Wyrażenia są opracowywane (wyliczane), a ich wyniki mogą być w różny sposób wykorzystane (np. w przypisaniu, jako argumenty innych operatorów, w instrukcjach sterujących wykonaniem programu, w wywołaniach metod).
Np. jeśli zmienna a ma wartość 1, zmienna b - wartośc 2, to:

Wyrażenie
Wynik
a
wartość zmiennej a ( =1 )
1
wartość literału 1 ( = 1)
a * (b + 10)
12
'a'
znak a (dziesiętnie liczba 91, która jest kodem znaku a)

Kolejność wyliczeń zależy od priorytetów i wiązań operatorów użytych w wyrażeniach.

Priorytety mówią o tym, w jakiej kolejności będą wykonywane różne operacje zawarte w tym samym wyrażeniu (np. czy w wyrażeniu a + b * c najpierw będzie wykonywane mnożenie czy dodawanie).

Wiązania określają kolejność wykonywania operacji o tym samym priorytecie - czy są wykonywane od lewej strony wyrażenia czy od prawej (np. czy wyrażenie a + b - c będzie traktowane jako (a+b)-c czy jako (b-c)+a - co nie zawsze - chociaż nie w tym przypadku - jest kwestią obojętną !).

Operatory mają argumenty, inaczej też zwane operandami.
W Javie są operatory jedno- i dwu- argumentowe oraz jeden operator trzyargumentowy.
Argumentami operatorów zawsze są wyrażenia. Np. w wyrażeniu
a + (b - 1)
argumentami dwuargumentowego operatora + są wyrażenia a i (b-1),
natomiast w wyrażeniu b-1 argumentami dwuargumentowego operatora - (minus) są wyrażenia b i 1.

W Javie zdefiniowano następujące operatory.

Priorytet,Wiązanie
OperatorNazwa
1, prawe





!Negacja logiczna 
~Bitowe uzupełnienie do 1
+Jednoargumentowy + (liczba dodatnia)
-Jednoargumentowy - (liczba ujemna)
++Operator zwiększania
--Operator zmniejszania
(typ)konwersja (rzutowanie)
2, lewe



*mnożenie
/dzielenie
%reszta z dzielenia
3, lewe


+Dodawanie
-Odejmowanie
4. lewe



<<Przesunięcie bitowe w lewo
>>Przesunęcie bitowe w prawo
>>>Przesunęcie bitowe w prawo bez znaku
5, lewe





<

operatory relacyjne




<=
>=
>
instanceofstwierdzenie typu
6, lewe


==Operatory równości - nierówności
!=
7.&Bitowa koniunkcja 
8.^Bitowe wyłączające ALBO
9.|Bitowa alternatywa
10.&&Logiczna koniunkcja
11.||Logiczna alternatywa
12, prawe?:Operator warunku (trójargumentowy)
13. prawe










=
*=
/=
%=
+=
-=
&=
^ =
|=
<< =
>>=

Operatory przypisania










Uwagi:  
mniejsza liczba w kolumnie priorytetu oznacza wyższy priorytet
niebieski kolor tła - operatory jednoargumentowe, biały - dwu

Nie będziemy teraz omawiać wszystkich tych operatorów, po części uwagi na temat niektórych z nich były już wypowiedziane, o innych będziemy się dowiadywać sukcesywnie.

Kilka uwag na tle przedstawionej tablicy warto jednak sformułować.

5.2. Operatory przypisania

Zauważmy najpierw, że operatory przypisania jako jedyne wsród operatorów dwuargumentowych mają wiązanie prawostronne. Oznacza to, że wyrażenie:

x = a + b

opracowywane jest od prawej do lewej. W takim wyrażeniu nie ma to istotnego znaczenia, ale jeśli uwzględnimy to, że samo przypisanie jest wyrażeniem o wartości równej jego lewej stronie (ktora zawsze musi być zmienną) po wykonaniu przypisania, to np. w takiej sekwencji:

int x, a = 3, b = 4;
int c = 1;
x = a = b = c + 7

po pierwsze, priorytet operatora + jest wyższy od priorytetu operatora =, wobec tego
najpierw zostanie wyliczone wyrażenie c + 7, jego wartość zostanie podstawiona na zmienną b, wartością wyrażenia b = c + 7 będzie teraz ta właśnie wartość (8) i kolejno - przesuwając się do lewej strony całego wyrażenia będzie ona podstawiona na zmienne a i x. W rezultacie zmienne x, a, b będą mialy taką samą wartość 8.

Jak widzimy w tablicy operatorów, występuje cała grupa tzw. złożonych operatorów przypisania w postaci:

                op=

gdzie op - to jeden z operatorów

 *        /        %        +        –        <<    >>    >>>        &        ^        |

Złożone operatory przypisania, stosowane w następujący sposób:

            x op= wyrażenie


 są wygodną formą skrócenia zapisu:

            x = x op ( wyrażenie )

gdzie:
x - dowolna zmienna
wyrażenie - dowolne wyrażenie
op - symbo operatora


Na przykład, zamiast:

numOfChildren = numOfChildren + 1

możemy napisac:

numOfChildren += 1


5.3. Zwiększanie i zmniejszanie

Wśród operatorów arytmetycznych szczególną rolę odgrywają jednoargumentowe  operatory zwiększania (++) i zmniejszania (--). Oba występują w dwóch postaciach:
Przy czym :

Na przykład zapis:

int n, i = 1;
n = i++;      // przyrostkowa forma operatora ++

interpretowany jest w następujący sposób: zmienna i zostanie zwiększona o 1, ale zmiennej n zostanie nadana wartość zmiennej i sprzed zwiększenia, czyli po wykonaniu obu instrukcji: n będzie równe 1 , a  i  będzie równe 2.

Natomiast:
int n, i = 1;
n = ++i;      // przedrostkowa forma operatora ++

interpretowany jest w następujący sposób: zmienna i zostanie zwięksozna o 1i ta nowa jej wartosć zostanie przypsiana zmiennej n,  czyli po wykonaniu obu instrukcji: n = 2 , i = 2.

Oczywiście, jeśli jedynym wyrażeniem w instrukcji jest wyrażenie zwiększania (lub zmniejszania), to rozróznienie przyrostkowej i przedrostkowej form tych operatorów nie ma znaczenia:

i++;
znaczy to samo, co
++i;
oraz
i = i + 1


Gdy nie jesteśmy absolutnie pewni kolejności opracowania wyrażeń, należy unikać stosowania operatorów ++ oraz -- w wyrażeniach, w których występują inne wyrażenia.


Zamiast pisać:

if (n++ > 2) ...;

lepiej jest zawsze napisać:

n++;
if (n > 2) ...

Na pewno zaś nigdy nie należy pisać (choć jest to dopuszczalne), czegoś w rodzaju:

x = i++*++j;

Pelniejszemy zrozumieniu przedstawionych uwag może posłużyć analiza dzialania poniższego programu.
public class Express1 {

 public static void main(String[] args) {

 int a = 1, b = 2, c = 3;
 a = b = c * 1 + 2;
 System.out.println("a=" + a + " b=" + b + " c=" + c);
 a = b = c * (1 + 2);
 System.out.println("a=" + a + " b=" + b + " c=" + c);
 a = b++;
 System.out.println("a=" + a + " b=" + b + " c=" + c);
 c = --b;
 System.out.println("a=" + a + " b=" + b + " c=" + c);
 a++;
 b++;
 c++;
 System.out.println("a=" + a + " b=" + b + " c=" + c);

 a = b++*++c;
 System.out.println("a=" + a + " b=" + b + " c=" + c);

 int DlugaNazwaZmiennej = 20;
 DlugaNazwaZmiennej = DlugaNazwaZmiennej * 10;
 DlugaNazwaZmiennej *= 10;
 System.out.println( DlugaNazwaZmiennej );

 }
}
Wydruk
a=5 b=5 c=3
a=9 b=9 c=3
a=9 b=10 c=3
a=9 b=9 c=9
a=10 b=10 c=10
a=110 b=11 c=11
2000
W programie tym stosujemy operator +  nie tylko do dodawania liczb. Służy on także konkatenacji (łączeniu łańcuchów znakowych). O szczegółach dowiemy się w następnym wykładzie.

5.4. Dzielenie całkowite i reszta

Inne operatory arytmetyczne były już po części omówione. Jeszcze raz warto jednak podkreślić, że dzielenie dla liczb całkowitych odrzuca ułamkową część wyniku.

Np.
int a,b,c;
b = 1; c = 10; a = b/c;   // a = 0, bo "prawdziwy" wynik dzielenia jest 0 i 1/10
a = 19/10;                    // a = 1, bo "prawdziwy" wynik dzielenia jest 1 i 9/10

Ogólnie więc, gdy b i c są zmiennymi typu int nie spelniona jest intuicyjna zależność matematyczna:

b  równa się c razy b dzielone przez c

Będzie ona zachodzić na pewno wtedy, gdy b dzieli się bez reszty przez c
Jeśli b nie dzieli się bez reszty przez c, to możemy otrzymać "prawdziwy" wynik gdy iloczyn c*b dzieli się bez reszty przez c.

Np.

b = 30; c = 20; b = c*b/c;    //  b = 30 

ale:

b = 30; c = 20; b = c*(b/c);  // b = 20 !

Tutaj widzimy jak nawiasy zmieniają kolejność obliczania wartości wyrażenia.
Wiązanie operatorów * i / jest lewostronne i w pierwszym przypadku cała operacja
wykonuje się w następujący sposób:
c*b daje 600 co dzielone przez c = 20 daje równo 30.

W drugim przypadku najpierw wykonane zostanie dzielenie w nawiasach (30/20 da 1), a wynik pomnożony przez wartość c (20) da 20.

Wbrew pozorom "dziwne" dzielenie całkowitoliczbowe może być użyteczne, szczególnie wraz z zastosowaniem operatora reszty z dzielenia.
Zobaczmy przykład.

Zadanie.
Mamy pojemniki. Do pojemników wkładamy klocki. Kiedy zabraknie miejsca w pojemniku - bierzemy następny. W każdym pojemniku mieści się s klocków.
Napisać program, który podaje ile potrzeba pojemników, aby przechować n klocków i ile klocków jest w ostatnim użytym pojemniku.

Uwaga: warto to zadanie rozwiązać samodzielnie, a dopiero później obejrzeć rozwiązanie.
 
Rozwiązanie.

Zadanie to można rozwiązać na wiele sposobów. Najbardziej zwartym i eleganckim jest wykorzystanie właściwości operatora dzielenia całkowitoliczbowego oraz operatora reszty z dzielenia.

Gdybyśmy rozwiązywali to zadanie "z kartką i ołówkiem", to liczbę potrzebnych pojemników uzyskamy poprzez podzielenie liczby klocków przez pojemność jednego pojemnika.  Jeżeli klocków jest 10,  a w każdym pojemniku mieści się co najwyżej 5 klocków, to liczba potrzebnych pojemników jest równa 10/5 = 2. Jednak gdy klocków będzie 11, to dzielenie da wynik 11/5 = 2  i 2/10. Ponieważ operujemy tylko całymi pojemnikami *nie ma czegoś takiego jak "kawałek" pojemnika) – potrzebny jest trzeci pojemnik, który będzie przechowywał jedenasty klocek (który nie zmieścił się w dwóch już wypełnionych pojemnikach).

To samo możemy zapisać w Javie. Ze względu na właściwości operatora dzielenia dla liczb całkowitych, wynik dzielenia całkowitoliczbowego pokaże nam ile jest pełnych pojemników.

Musimy się jeszcze dowiedzieć czy przypadkiem nie jest potrzebny pojemnik dodatkowy.

O tym powie nam reszta z dzielenia liczby klocków przez pojemność pojemnika. Jeśli ta reszta równa jest 0 – mamy tylko pełne pojemniki.  W przeciwnym razie – potrzebny jest dodatkowy pojemnik, w którym umieszczone zostaną ostatnie klocki. Ten dodatkowy pojemnik nie będzie całkowicie wypełniony i  powinniśmy umieć powiedzieć ile jest w nim klocków. Oczywiście – ta liczba klocków równa jest wynikowi operacji reszty z dzielenia.

Poniższy wydruk przedstawia omówiony program.
public class Klocki {
 
  public static void main(String[] args) {

    int n = 41;         // liczba klocków

    int s = 7;          // ile klocków mieści się w pojemniku
                        // jezeli wszystkie pojemniki sa pelne pokazuje 
                        // również ile jest klockow w ostatnim z nich
                        // wykorzystamy te zmienną do pokazania
                        // ile jest klocków w ostatnim pojemniku
                         
    int poj  = n/s;     // liczba pojemników; inicjalnie - pełnych
    int rest = n%s;     // reszta z dzielenia pokazuje 
                        // czy mamy dodatkowy pojemnik 
                        // i ile jest w nim klocków

    if (rest != 0) {    // jeżeli jest dodatkowy pojemnik
       poj++;           // liczba pojemników zwiekszona o dodatkowy
       s = rest;        // zmieniamy wartosc s - ostatni pojemnik niepelny!
    }
    
    System.out.println("Liczba pojemnikow        : " + poj); 
    System.out.println("Liczba klockow w ostatnim: " + s);
  }

}
Da on następujący wynik.
Liczba pojemnikow        : 6
Liczba klockow w ostatnim: 6


Demonstracja programu Klocki pozwala na wprowadzanie dowolnych danych. Zobacz demo.


W programie pojawia się kilka nowych elementów języka. Będą one szczegółowo omawiane dalej, teraz dla zrozumienia dzialanie programu wystarczy kilka komentarzy:


6. Konwersje arytmetyczne

Ogólnie, jeżeli zmienna jest typu A to nie można jej przypisać wartości typu B. Np. taki zapis w programie spowoduje błąd w kompilacji:

int a = "Ala ma kota";

gdyż a jest zmienną typu int, a wyrażenie "Ala ma kota" jest literałem łańcuchowym i  jest typu  String.

Byłoby jednak nierozumnie stosować tę zasadę w całej rozciągłości wobec typów arytmetycznych. Przecież to są liczby i często chcielibyśmy jakąś wartość liczbową przypisać zmiennej typu arytmetycznego nie bacząc na typy.

Zmiana typu wyrażenia nazywa się konwersją lub - inaczej - rzutowaniem (ang. cast)


Te konwersje arytmetyczne, które nie mogą doprowadzić do utraty informacji reprezentowanej przez wartość wyrażenia dokonywane są w sposób niejawny, niejako automatyczny.
Możemy np. bez przeszkód przypisać zmiennej typu double wartość zmiennej typu int:

int a = 10;
double d = a;

bowiem każda wartość zmiennej typu int "zmieści się" w zmiennej typu double.

Takie konwersje nazywają się rozszerzającymi, bo przekształcane są "węższe" typu do "szerszych" (mogących reprezentować większe spektrum wartości).

Gdybyśmy jednak chcieli wykonać operację odwrotną:

double d = 10.1;
...
int a = d;

to kompilator będzie sygnalizował błąd. Taki zapis jest niedopuszczalny, ponieważ przy  okazji takiego przypisania możemy stracić informację zawartą w zmiennej "szerszego" typu.
Konwersja w tym przypadku nazywa się konwersją zawężającą i możliwa jest do wykonania tylko pod warunkiem użycia operatora konwersji. Mówimy, że konwersje zawężające muszą być wykonywane jawnie, za pomocą operatora konwersji. Użycie operatora konwersji oznacza, że programista świadomie decyduje się na przekstzalcenie typów, przy którym może nastąpić strata informacji. Że nie jest to przypadkowy błąd w programie.

Operator konwersji (rzutowania) ma postać:

        (nazwa_typu) wyrażenie

gdzie nazwa_typu - typ do jakiego przekształcany jest aktualny typ wyrażenia


W poprzednim przykładzie powinniśmy więc napisać:

double d = 10.9;
...
int a = (int) d;

i teraz kompilator pozwoli nam na pozostawienie takiego zapisu w programie.

Warto zwrócić uwagę, że przy tej okazji może faktycznie nastąpić utrata informacji. W naszym przykładzie zmienna a będzie miała wartośc 10 - straciliśmy całą część ułamkową liczby. Podkreślmy: to nie jest zaokrąglenie, część ulamkowa jest po prostu odrzucana. Oczywiście nie zawsze informacja będzie tracona (gdy d = 10.0, to a = 10 i mamy praktycznie to samo).

Bardzo często przy operowaniu na znakach (typo char) i ich kodach liczbowych potrzebne jest wykonywanie konwersji.
Obrazuje to poniższy program:

public class Char {

 public static void main(String[] args) {

 char c = 'a';
 int kod = c;
 System.out.println("Kod znaku " + c + " = " + kod);
 kod = 77;
 c = (char) kod;
 System.out.println("Kod znaku " + c + " = " + kod);

 // Czasem trzeba użyć jawnej konwersji rozszerzającej
 System.out.println("Kod znaku " + '*' + " = " + (int) '*');

 // Oczywiście - zawężające zawsze trzeba podawać jawnie
 System.out.println("Kod znaku " + (char) 66 + " = " + 66);

 }

}
który wyprowadzi na konsolę następującą informację:
Kod znaku a = 97
Kod znaku M = 77
Kod znaku * = 42
Kod znaku B = 66


W trakcie wyliczania wartości wyrażeń arytmetycznych dokonywane są automatyczne konwersje nazywane promocjami numerycznymi.

Promocja numeryczna polega na przekształceniu typów argumentów operatorów arytmetycznych do typu wspólnego dla ich argumentów


Odpowiedź na pytanie jaki jest ten wspólny typ daje następująca reguła.

Promocje argumentów operatorów dwuargumentowych są wykonywane w następujący sposób:
 


7. Podsumowanie

W rozdziale omówiono:
Ta dość formalna wiedza pozwala już poprawnie pisać proste programy.
Ale przyzwyczajenie poprawnego deklarowania zmiennych i używania literałów będzie owocować i dalej, kiedy pisane programy staną sie bardziej złożone.