Języki programowania - ćwiczenia 7
Temat zajęć: Programowanie w języku Java: wprowadzenie, struktura programu, pakiety, interfejsy.
Materiały dodatkowe:
Struktura i organizacja programu w Javie
Programy w języku Java składają się z klas i interfejsów rozmieszczonych w pakietach. Pakiety mogą być zagnieżdżone. Nazwy pakietów (podpakietów) i klas muszą być unikalne w ramach jednego pakietu.
Język Java dopuszcza kwalifikację dostępu do klasy:
- klasy chronione (bez specyfikacji dostępu) dostępne są jedynie dla klas z tego samego pakietu
- klasy publiczne (oznaczone klauzulą public) dostępne są dla wszystkich klas, również spoza pakietu
Aby kompilować programy w języku Java potrzebny jest kompilator tego języka (np. javac z pakietu JDK (Java Development Kit) firmy Sun Microsystems, gcj z GNU gcc, guavac itp.). Klasy kompilowane są do postaci tzw. bytecode,
który ma taki sam format na wszystkich platformach (zapewnia to
przenośność programów w Javie na poziomie kodu binarnego).
Do uruchomienia bytecode'u Javy potrzebny jest program implementujący JVM (Java Virtual Machine), np. java z JDK firmy Sun. Faktycznie więc programy w Javie nie mogą działać samodzielnie, potrzebny jest interpreter lub JIT (just-in-time compiler; program zamieniający w locie bytecode Javy na kod natywny na danej platformie) oraz maszyna wirtualna,
zapewniająca programom w Javie odpowiednie środowisko pracy. Programy w
Javie można więc uruchamiać na tych platformach, dla których
istnieje pakiet implementujący JVM.
Pełna nazwa kwalifikowana klasy w języku Java ma postać: specyfikacja_pakietu.nazwa_klasy. Na przykład java.util.Vector oznacza klasę o nazwie Vector, zdefiniowaną w pakiecie util, który jest częścią pakietu java. Zwyczajowo nazwy pakietów piszemy z małej litery, a nazwy klas z wielkiej.
W pakiecie JDK znajduje się biblioteka wielu predefiniowanych, gotowych
do użycia klas, pogrupowanych w szereg pakietów. Dokładna
definicja i przeznaczenie klas opisane jest w dokumentacji JDK.
Ważniejsze (częściej używane pakiety):
- java.lang - podstawowe klasy, interfejsy i wyjątki języka
- java.io - obsługa wejścia/wyjścia i systemu plików
- java.net - obsługa sieci
- java.awt i javax.swing - graficzny interfejs użytkownika
- java.util - zbiór klas ułatwiających programowanie (wektory, tablice asocjacyjne itp.)
- java.rmi - zdalne wywołania metod
W języku Java wszystkie klasy dziedziczą z java.lang.Object, nawet jeśli nie jest to jawnie podane w kodzie źródłowym.
Przykład 1
Podstawowe elementy programu w języku Java.
Plik uam/wmi/jpr/Przyklad1.java pobierz
package uam.wmi.jpr;
import java.io.*;
public class Przyklad1 {
String pole1;
public String pole2;
public Przyklad1() {
pole1 = "ABCDE";
pole2 = new String("FGHIJ");
}
void pisz() {
System.out.println(pole1);
}
public void piszWszystko() {
System.out.println(pole1+" "+pole2);
}
public static void main(String args[]) {
System.out.println("Podano "+args.length+" parametrow");
Przyklad1 p = new Przyklad1();
p.piszWszystko();
p.pisz(); // można, bo main() też należy do klasy Przyklad1
}
}
Program składa się z jednej klasy o nazwie Przyklad1. Jest
to klasa publiczna, ponieważ tylko takie klasy mogą być "uruchamiane"
przez użytkownika (klasy chronione mogą być jedynie wykorzystywane
przez inne klasy z tego samego pakietu).
Wykonanie programu zaczyna się od publicznej, statycznej metody main. Parametry tej metody to parametry podane przez użytkownika w wierszu poleceń przy uruchamianiu programu.
Klasa Przyklad1 znajduje się w pakiecie jpr, który jest częśćią pakietu wmi, który z kolei jest częścią pakietu uam.
Hierarchia pakietów musi być odzwierciedlona w hierarchii
katalogów, zatem jeśli uznamy, że bieżącym katalogiem jest J:\, to należy stworzyć folder uam, w nim folder wmi, w nim folder jpr, a w folderze jpr zapisać klasę Przyklad1 w pliku Przyklad1.java.
Nazwa pliku kodu źródłowego musi być dokładnie taka sama (nawet
wielkość liter ma znaczenie), jak nazwa publicznej klasy, która
się w tym pliku znajduje (implikuje to fakt, że w jednym pliku
źródłowym nie mogą znajdować się dwie różne klasy
publiczne).
Uruchamiać klasę należy z folderu "nad" uam, poleceniem
java uam.wmi.jpr.Przyklad1 [parametry]
Przy uruchamianiu programów w Javie kluczową rolę odgrywa zmienna środowiskowa CLASSPATH. Mówi ona maszynie wirtualnej, w których katalogach ma ona poszukiwać bytecode'ów klas. Jeśli nasza klasa Przyklad1.class znajduje się w katalogu j:\java\uam\wmi\jpr, to albo w CLASSPATH musi znaleźć się katalog j:\java, albo katalog . (bieżący) i wtedy należy uruchamiać klasę pracując w katalogu j:\java.
Kolejne katalogi w CLASSPATH oddzielamy średnikami (Windows) lub dwukropkami (Unix).
Oczywiście przed uruchomieniem programu należy klasę Przyklad1 skompilować do postaci bytecode'u. Kompilację przeprowadzamy korzystając z programu javac, wchodzącego w skład JDK:
javac uam\wmi\jpr\Przyklad1.java
Proszę
zauważyć, że tym razem podajemy lokalizację plików zawierających
kod źródłowy w postaci zwykłej ścieżki dostępu, podając
również rozszerzenie pliku.
Podstawowe typy danych i instrukcje
Język Java oferuje następujące typy proste (skalarne):
- typy całkowite: byte, int, long
- typ logiczny: boolean
- typ znakowy: char
- typy rzeczywiste: float, double
Wszystkie inne typy to typy obiektowe (klasy wywodzące się z java.lang.Object), łącznie z typem łańcuchowym (tekstowym) String. Tablice w języku Java również są obiektami (obojętnie jakiego typu są ich elementy).
Nowe obiekty tworzymy za pomocą operatora new, który może przyjmować parametry (uruchamiany jest wtedy konstruktor zgodny z podanymi parametrami).
W JĘZYKU JAVA NIE ISTNIEJE MECHANIZM DYNAMICZNEGO ZWALNIANIA
PAMIĘCI. OBIEKTY USUWANE SĄ Z PAMIĘCI AUTOMATYCZNIE KIEDY NIE ISTNIEJĄ
JUŻ ŻADNE REFERENCJE, KTÓRE NA NIE WSKAZUJĄ. PROCES TEN NAZYWA
SIĘ ODŚMIECANIEM (GARBAGE COLLECTION).
Innymi słowy język Java nie posiada odpowiednika delete z C++, free z C czy dispose z Pascala.
Formalnie każda klasa w kodzie źródłowym powinna być
kwalifikowana nazwą pakietu. Wydłużałoby to jednak znacznie kod
programu, dlatego możliwe jest zaimportowanie wszystkich lub wybranych
nazw klas z danego pakietu:
import java.util.Vector;
Importuje nazwę klasy Vector z pakietu java.util. Jeśli ta instrukcja znajduje się na początku kodu źródłowego, to w danym module możemy odwoływać się do klasy java.util.Vector po prostu przez Vector. Instrukcja import jest zatem odpowiednikiem instrukcji using namespace z języka C++. Użycie import nie powoduje dołączenia klas do danego modułu - wpływa ona wyłącznie na zakres widocznośći i nazewnictwo.
import java.util.*;
Importuje wszystkie nazwy klas z pakietu java.util. Możemy więc korzystać z klas z tego pakietu (np. Hashtable, StringTokenizer, Vector, ...) nie kwalifikując ich nazwą pakietu.
Domyślnie przyjmowane jest zawsze import java.lang.*;
Instrukcje
- instrukcja wyboru:
if (warunek) instrukcje1; else instrukcje2;
switch (wyrazenie) {
case w1 : instrukcje1; [break;]
...
default: instrukcje;
}
- pętle:
for(wart_pocz; war_konc; zmiana) { instrukcje }
while (warunek) { instrukcje }
do { instrukcje } while (warunek);
- instrukcja przechwytywania wyjątków:
try {
instrukcje;
} catch (def_wyjatku) {
obsluga_wyjatku
}
- instrukcja synchronizowanego dostępu:
synchronized(obiekt) {
instrukcje
}
Przykład 2
Program wyświetlający na konsoli zawartość pliku tekstowego o podanej nazwie. Wykorzystuje klasy z pakietu java.io.
Plik WyswietlPlik.java pobierz
public class WyswietlPlik {
String nazwa;
public WyswietlPlik(String n) {
nazwa = n;
}
public void wyswietl() {
try {
java.io.BufferedReader r =
new java.io.BufferedReader(new java.io.FileReader(nazwa));
String s;
while ( (s=r.readLine()) != null ) {
System.out.println(s);
}
} catch (java.io.IOException ioe) {
System.out.println("Przechwycony wyjatek I/O: "+ioe.getMessage());
ioe.printStackTrace();
} catch (Exception e) {
System.out.println("Przechwycony wyjatek: "+e.getMessage());
e.printStackTrace();
}
}
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Musisz podac nazwe pliku.");
System.exit(1);
}
WyswietlPlik w = new WyswietlPlik(args[0]);
w.wyswietl();
}
}
Interfejsy i implementowanie interfejsów
Język Java nie przewiduje dziedziczenia wielobazowego (każda klasa
ma dokładnie jedną klasę bazową). Jako swego rodzaju ekwiwalent,
istnieje możliwość definiowania interfejsów
(przypominających klasy abstrakcyjne) oraz ich implementację w klasach
(klasa może implementować dowolnie wiele interfejsów).
Przykład 3
Prosty przykład obrazujący sposób definicji i implementacji interfejsów.
Plik MozePisac.java pobierz
public interface MozePisac {
public void pisz();
}
Definicja interfejsu MozePisac mówi tyle, że z obiektu każdej klasy implementującej ten interfejs można wywołać metodę pisz.
Plik Osoba.java pobierz
public class Osoba {
public String imie;
public String nazwisko;
public Osoba(String i, String n) {
imie = new String(i);
nazwisko = new String(n);
}
}
Klasa definiująca obiekty przechowujące dane osobowe (imię i
nazwisko). Zaimplementowany konstruktor przyjmujący dane osobowe jako
parametry.
Plik Student.java pobierz
public class Student
extends Osoba
implements MozePisac {
public Student(String im, String naz) {
super(im,naz);
}
public void pisz() {
System.out.println(imie+" "+nazwisko);
}
}
Klasa rozszerzająca (extends) klasę Osoba (każdy obiekt klasy Student ma zatem pola imie i nazwisko) i implementująca (implements) interfejs MozePisac (z każdego obiektu klasy Student można zatem wywołać metodę pisz)..
Jeśli dana klasa implementuje interfejs, to w jej ciele muszą zostać
zaimplementowane wszystkie metody określone w tym interfejsie, chyba że
część z nich została odziedziczona z klasy bazowej. W konstruktorze
klasy Student wywołujemy konstruktor klasy bazowej (superclass) Osoba za pomocą słowa kluczowego super.
Plik Glowny.java pobierz
public class Glowny {
public static void main(String[] args) {
MozePisac s = new Student(args[0],args[1]);
s.pisz();
}
}
Główna klasa uruchomieniowa programu. Proszę zauważyć, że
jeśli nie podamy co najmniej dwóch parametrów w wierszu
poleceń (można to sprawdzić eksperymentalnie), program zakończy się
wyjątkiem java.lang.ArrayIndexOutOfBoundsException (przekroczony rozmiar tablicy), ponieważ w takim przypadku odwołujemy się do nieistniejących pól tablicy args.
Kompilacja przykładu:
javac *.java
Uruchomienie przykładu:
java Glowna jakies_imie jakies_nazwisko
Zadanie 1
Napisać program w języku Java, który wypisze wszystkie
parametry podane w wierszu polecenia w czasie jego uruchomienia w
odwrotnej kolejności.
Wskazówki:
Przykład 4
Zaimplementujemy teraz klasę, której obiekty będą utożsamiać liczby zespolone. Klasa będzie nosiła nazwę LZesp, a jej przykładowa implementacja podana została poniżej.
Plik LZesp.java pobierz
class LZesp {
private double re;
private double im;
public LZesp() {
re = 0.0;
im = 0.0;
}
public LZesp(double are, double aim) {
re = are;
im = aim;
}
public double getRe() {
return re;
}
public double getIm() {
return im;
}
public void pisz() {
System.out.print(jakoLancuch());
}
public String jakoLancuch() {
return new String(re+" + "+im+"i");
}
public LZesp plus(LZesp l) {
return new LZesp(re + l.getRe(), im + l.getIm());
}
public LZesp minus(LZesp l) {
return new LZesp(re - l.getRe(), im - l.getIm());
}
public LZesp razy(LZesp l) {
return new LZesp(
re * l.getRe() - im * l.getIm(),
re * l.getIm() + im * l.getRe()
);
}
}
Klasa posiada dwa pola: re i im, reprezentujące odp.
część rzeczywistą i urojoną liczby zespolonej. Do pobierania wartości
tych pól (są one prywatne, więc kod spoza klasy LZesp nie ma do nich bezpośredniego dostępu) przez inne obiekty służą metody getRe() i getIm() (często stosuje się zasadę, aby ograniczać bezpośredni dostęp do pól oferując w zamian odpowiednie metody).
Klasa posiada dwa konstruktory. Konstruktor bezparametrowy LZesp() nadaje polom re i im wartość 0, natomiast konstruktor LZesp(double,double) wstawia do pól zadane wartości.
Metoda jakoLancuch() służy do reprezentacji danej liczby zespolonej w formie łańcucha tekstowego, a korzystająca z niej metoda pisz wypisuje daną liczbę zespoloną na standardowe wyjście.
Metody plus(LZesp), minus(LZesp) i razy(Lzesp) odp. dodają, odejmują i mnożą daną liczbę zespoloną oraz liczbę podaną jako parametr metody. Wynikiem jest nowy obiekt klasy LZesp.
Obejrzyjmy przykład zastosowania klasy LZesp.
Plik Przyklad4.java pobierz
class Przyklad4 {
public static void main(String args[]) {
LZesp l1 = new LZesp(3,2);
LZesp l2 = new LZesp(2,3);
System.out.println(l1.jakoLancuch() + " + " +
l2.jakoLancuch() + " = " +
l1.plus(l2).jakoLancuch()
);
System.out.println(l1.jakoLancuch() + " - " +
l2.jakoLancuch() + " = " +
l1.minus(l2).jakoLancuch()
);
System.out.println(l1.jakoLancuch() + " * " +
l2.jakoLancuch() + " = " +
l1.razy(l2).jakoLancuch()
);
}
}
Po uruchomieniu przykładu powinniśmy otrzymać jako wynik:
3.0 + 2.0i + 2.0 + 3.0i = 5.0 + 5.0i
3.0 + 2.0i - 2.0 + 3.0i = 1.0 + -1.0i
3.0 + 2.0i * 2.0 + 3.0i = 0.0 + 13.0i
Zauważmy, że środkowy wiersz 1.0 + -1.0i nie wygląda zbyt elegancko. Dzieje się tak dlatego, że metoda jakoLancuch() naszej klasy LZesp nie jest najwyższej jakości (nie sprawdza np. czy dany człon jest dodatni, ujemny czy równy 0).
Zaimplementujmy drugą klasę, nazwaną LZesp2, która będzie rozszerzać LZesp i będzie zawierać inną implementację metody jakoLancuch().
Plik LZesp2.java pobierz
class LZesp2 extends LZesp {
public LZesp2(double are, double aim) {
super(are, aim);
}
public LZesp2(LZesp l) {
super(l.getRe(), l.getIm());
}
public String jakoLancuch() {
String s = new Double(getRe()).toString();
if (getIm() < 0) {
s += " - " + Math.abs(getIm()) + "i";
} else {
s += " + " + getIm() + "i";
}
return s;
}
}
Konstruktor LZesp2(double,double) wywołuje konstruktor klasy nadrzędnej LZesp(double,double) za pomocą słowa kluczowego super. Dodatkowo zaimplementowany został konstruktor LZesp2(LZesp), który umożliwia utworzenie liczby zespolonej na podstawie innej liczby zespolonej.
Wreszcie przeimplementowana została metoda jakoLancuch(), która tym razem sprawdza znak części urojonej i albo używa znaku "+", albo "-".
Poniższy przykład pokazuje wykorzystanie klasy LZesp2.
Plik Przyklad4_2.java pobierz
class Przyklad4_2 {
public static void main(String args[]) {
LZesp l1 = new LZesp(3,2);
LZesp l2 = new LZesp(2,3);
System.out.println(l1.jakoLancuch() + " + " +
l2.jakoLancuch() + " = " +
l1.plus(l2).jakoLancuch()
);
System.out.println(l1.jakoLancuch() + " - " +
l2.jakoLancuch() + " = " +
new LZesp2(l1.minus(l2)).jakoLancuch()
);
System.out.println(l1.jakoLancuch() + " * " +
l2.jakoLancuch() + " = " +
l1.razy(l2).jakoLancuch()
);
}
}
Zauważmy, że jedyna zmiana w przykładzie występuje w wierszu 13:
new LZesp2(l1.minus(l2)).jakoLancuch()
i polega na utworzeniu nowego obiektu klasy LZesp2 na podstawie wyniku odejmowania l1.minus(l2) (wykorzystany jest konstruktor LZesp2(LZesp)) i skorzystaniu z jego metody jakoLancuch() (czyli z metody LZesp2.jakoLancuch()), która nieco ładniej prezentuje liczby z ujemną częścią urojoną. Obiekt klasy LZesp2 zostanie następnie (po zakończeniu wywołania System.out.println) usunięty przez garbage collector, ponieważ nigdzie w programie nie zapamiętujemy jego referencji.
Jako wynik działania Przyklad3 otrzymujemy:
3.0 + 2.0i + 2.0 + 3.0i = 5.0 + 5.0i
3.0 + 2.0i - 2.0 + 3.0i = 1.0 - 1.0i
3.0 + 2.0i * 2.0 + 3.0i = 0.0 + 13.0i
Niestety w trzecim wierszu nadal występuje nieładny zapis 0.0 + 13.0i; chcielibyśmy aby w takim przypadku reprezentacją tekstową było po prostu 13.0i. Problem ten rozwiążą Państwo tworząc kolejnego potomka klasy LZesp.
Zadanie 2
Zaimplementować klasę LZesp3 dziedziczącą z LZesp, umieścić w niej konstruktor LZesp3(LZesp) oraz przeimplementować metodę jakoLancuch tak, aby spełniała następujące wymagania:
- jeśli część rzeczywista jest równa 0, to jest ona pomijana w reprezentacji tekstowej,
- jeśli część urojona jest równa 0, to jest ona pomijana w reprezentacji tekstowej,
- jeśli obie częśći są równe 0, to wynikiem ma być łańcuch "0",
- jeśli część urojona jest ujemna, a część rzeczywista występuje, to wynik ma być taki jak w przypadku LZesp2.jakoLancuch(),
- jeśli część urojona jest ujemna i równa -x, a część rzeczywista równa 0, to wynikiem ma być "-xi", a nie "- xi".
Zaimplementować klasę przykładową Przyklad4_3, w której sprawdzone będą nowe własności klasy LZesp3.