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:
- zmienna ma nazwę (użyty
w programie symbol nazywa się nazwą zmiennej),
- przez
tę nazwę odwołujemy się do konkretnego obszaru pamięci,
w którym chcemy przechowywać wartości jakiejś danej,
- zawartość
tego obszaru (wartość zmiennej) możemy zmieniać w
trakcie wykonania programu
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:
- dodawanie
(operator +),
- mnożenie
(operator *),
- odejmowanie (operator -),
- dzielenie
(/),
- reszta z dzielenia (operator %)
- zwiększanie
o 1 (operator ++)
- oraz zmniejszanie
o 1 (--).
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:
3
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:
- każda liczba całkowita zapisana literalnie (np.
100) traktowana jest jako liczba typu int.
-
każda liczba rzeczywista zapisana literalnie (z kropką dziesiętną - np.
1.2,
albo w notacji naukowej np. 1e+9) traktowana jest jako liczba typu double,
- każdy
literał znakowy (znak w apostrofach) jest typu char
- każdy
napis true lub false jest
literałem logicznym (typ boolean); słowa true i false są
słowami zarezerwowanymi języka.
W niektórych przypadkach programista może zmienić interpretację
literałów liczbowych, stosując specjalne modyfikatory,
mianowicie:
- użycie litery L lub l jako przyrostka liczby
całkowitej sprawia, że
liczba ta będzie traktowana jako typu long (np. 3L - będzie literałem
typu
long)
- użycie f lub F spowoduje, że liczba
rzeczywista będzie traktowana jako typu float (np. 0.3f)
Liczby całkowite mogą być zapisywane:
- w systemie dziesiętnym - w naturalny sposób (np. 3
lub 121),
- w systemie ósemkowym - poprzez
poprzedzenie liczby znakiem 0 (np. 03),
-
w systemie szesnastkowym - poprzez poprzedzenie liczby znakami 0x lub
0X
(np. 0x1a, 0X11, 0xFF - zwróćmy uwagę, że cyfry szesnastkowe powyżej 9
mogą
być zapisywane dużymi lub małymi literami)
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:
- Operacje na liczbach rzeczywistych są niedokładne.
- W
wyrażeniach podanych w programie wystarczyło zmodyfikować tylko jeden
z literałów biorących udział w operacji, by otrzymać pożądany wynik(nie
pisaliśmy
10d/3d, wystarczyło napisac 10d/3). Intuicyjnie możemy to rozumieć w
ten
sposób, że typ wyniku operacji określany jest przez "większy"
(określający większe możliwe wartości) z typów biorących
w niej udział argumentów (w tym przypadku literałów).
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:
- literał znakowy - w apostrofach - to zawsze jeden
znak,
- literał łańcuchowy - w cudzysłowie - to zero
lub dowolna liczba znaków;
jeśli w cudzysłowie nie podamy żadnego znaku - to mamy pusty łańcuch
znakowy
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ę:
-
'\u03b1' oznacza (heksadecymalnie) kod znaku 'α' (alfa - z alfabetu
greckiego), inne kody w tym wierszu programu odpowiadają literom beta i
gamma,
-
znak apostrofu i lewy ukośnik w literale znakowym oznaczamy \' i \\
-
znaki specjalne zupełnie naturalnie wstawiamy do łańcuchów znakowych: w
napise
"\nalfa\nbeta\ngamma" \n oznacza znak nowego wiersza, wobec tego
napisy zostaną wyprowadzone w kolejnych wierszach,
-
aby wyprowadzić napis zawierający ukośniki musieliśmy je w tym napisie
powtarzać .
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?
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:
- nazwy zmiennych i metod - zaczynam małą
literą, każdy składnik wyróżniamy
rozpoczynając go dużą literą (np. x, price, numOfAllOccurs,
liczbaDzieciMlodszych,
increment(), getBackground())
- nazwy stalych -
piszemy dużymi literami, składniki nazwy rozróżniając za pomocą znaku
podkreślenia (np. NUM, EXIT_ON_CLOSE)
- nazwy klas
zaczynamy dużą literą, i dalej wyrózniamy poszczególne składniki nazwy
- też dużą literą (np. Frame, ArrayList)
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
| Operator | Nazwa
|
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
|
<= |
>= |
> |
instanceof | stwierdzenie 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:
- przyrostkowej (operator po argumencie - zmiennej)
- przedrostkowej
(operator przed argumentem - zmienną)
Przy czym :
- ++ zwiększa, a -- zmniejsza o jeden
wartość argumentu (zmiennej),
- przyrostkowa forma
operatorów (znak operatora po argumencie) modyfikuje
wartość argumentu po jej wykorzystaniu w wyrażeniu, przedrostkowa (znak
operatora
przed argumentem) - przed wykorzystaniem tej wartości.
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:
- instrukcja if służy do sprawdzania warunków, jeśli warunek w
nawiasach okrągłych jest prawdziwy, to jest wykonywana instrukcja
zapisana po zamykającym nawiasie okrągłym,
-
instrukcja grupująca { ... } służy do grupowania instrukcji - ujęte w
nawiasy
klamrowe traktowane są jako jedna instrukcja, co wykorzystujemy w
if
- operator "czy nie równe" zapisujemy w Javie jako !=
- operator
"czy równe" zapisujemy jako ==
- zauważmy, że
priorytet operatora % jest wyższy niż operatorów == i !=, dlatego w if
nie musieliśmy stosować dodatkowych nawiasów.
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:
- jeżeli jeden z argumentów jest typu double, drugi
przekształcany jest do typu double,
- w przeciwnym
razie, jeżeli jeden z argumentów jest typu float, drugi przekształcany
jest do typu float,
- w przeciwnym razie, jeżeli
jeden z argumentów jest typu long, drigi przekształcany jest do typu
long,
- w przeciwnym razie oba argumenty
przekształcane są do typu int.
7. Podsumowanie
W rozdziale omówiono:
- pojęcie literału i zmiennej,
- pojęcie typu danych,
- rodzaje
typów danych w Javie (w tym szczegółowo tzw. typy proste),
- literały,
- deklaracje
zmiennych,
- pojęcie identyfikatora i konwencje
nazewnicze Javy,
- podstawowe operatory
(w tym szczegółowo operatory arytmetyczne),
- pojęcie
wyrażenia oraz priorytety i wiązania operatorów,
- konwersje
arytmetyczne.
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.