3. Klasy i interfejsy
3.1. Obiekt na przykładzie
3.1.1. Informacje ogólne
Teraz zajmiemy się programowaniem obiektowym na przykładach. Obiekt to jednostka zawierająca dane określające jego stan (nazywane atrybutami lub właściwościami) oraz funkcje (nazywane metodami). Obiekt jest tworzony na podstawie wzorca zwanego klasą:
public class C1{
type1 p1; // właściwość p1
type2 p2; // właściwość p2
…
type3 m3(…){ // metoda m3
…
}
type4 m4(…){ // metoda m4
…
}
…
}
Na podstawie poprzedniej klasy C1 można utworzyć wiele obiektów O1, O2,… Wszystkie będą miały właściwości p1, p2,… oraz metody m3, m4,… Będą miały różne wartości dla swoich właściwości pi, dzięki czemu każdy z nich będzie miał swój własny stan.
Jeśli O1 jest obiektem typu C1, O1.p1 oznacza właściwość p1 obiektu O1, a O1.m1 oznacza metodę m1 obiektu O1.
Rozważmy pierwszy model obiektu: klasę personne.
3.1.2. Definicja klasy „osoba”
Definicja klasy personne będzie wyglądać następująco:
import java.io.*;
public class personne{
// atrybuty
private String prenom;
private String nom;
private int age;
// metoda
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
// metoda
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
Mamy tu do czynienia z definicją klasy, a więc typu danych. Kiedy będziemy tworzyć zmienne tego typu, będziemy je nazywać obiektami. Klasa jest zatem wzorcem, na podstawie którego tworzone są obiekty.
Członkowie lub pola klasy mogą być danymi lub metodami (funkcjami). Pola te mogą posiadać jeden z trzech następujących atrybutów:
privé: Pole prywatne (private) jest dostępne wyłącznie dla wewnętrznych metod klasy
public: Pole publiczne jest dostępne dla każdej funkcji, niezależnie od tego, czy została zdefiniowana w ramach klasy, czy nie
protégé: Pole chronione (protected) jest dostępne wyłącznie dla wewnętrznych metod klasy lub obiektu pochodnego (patrz później pojęcie dziedziczenia).
Zazwyczaj dane klasy są deklarowane jako prywatne, podczas gdy jej metody są deklarowane jako publiczne. Oznacza to, że użytkownik obiektu (programista)
a: nie będzie miał bezpośredniego dostępu do danych prywatnych obiektu
b: będzie mógł wywoływać publiczne metody obiektu, a w szczególności te, które zapewniają dostęp do jego danych prywatnych.
Składnia deklaracji obiektu jest następująca:
public class nomClasse{
private donnée ou méthode privée
public donnée ou méthode publique
protected donnée ou méthode protégée
}
Remarques
- Kolejność deklaracji atrybutów private, protected i public jest dowolna.
3.1.3. Metoda initialise
Wróćmy do naszej klasy „osoba” zadeklarowanej jako:
import java.io.*;
public class personne{
// atrybuty
private String prenom;
private String nom;
private int age;
// metoda
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
// metoda
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
Jaka jest rola metody initialise? Ponieważ nazwisko, imię i wiek są danymi prywatnymi klasy „osoba”, instrukcje
są niedozwolone. Musimy zainicjować obiekt typu personne za pomocą metody publicznej. Tą rolę pełni metoda initialise. Napiszemy:
Zapis p1.initialise jest poprawny, ponieważ initialise jest publicznie dostępny.
3.1.4. Operator new
Sekwencja instrukcji
jest nieprawidłowa. Instrukcja
deklaruje p1 jako odwołanie do obiektu typu personne. Obiekt ten jeszcze nie istnieje, a zatem p1 nie jest zainicjowany. To tak, jakbyśmy napisali:
gdzie za pomocą słowa kluczowego null wyraźnie wskazujemy, że zmienna p1 nie odwołuje się jeszcze do żadnego obiektu.
Kiedy następnie zapisujemy
wywołujemy metodę initialise obiektu, do którego odwołuje się zmienna p1. Jednak ten obiekt jeszcze nie istnieje, więc kompilator zgłosi błąd. Aby zmienna p1 odwoływała się do obiektu, należy zapisać:
Spowoduje to utworzenie jeszcze niezinicjalizowanego obiektu typu personne: atrybuty nom i prenom, które są odwołaniami do obiektów typu String, będą miały wartość null, a age – wartość 0. Ma więc miejsce inicjalizacja domyślna. Teraz, gdy p1 odwołuje się do obiektu, instrukcja inicjalizacji tego obiektu
jest poprawna.
3.1.5. Słowo kluczowe „this”
Przyjrzyjmy się kodowi metody initialise:
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
Instrukcja this.prenom=P oznacza, że atrybut prenom bieżącego obiektu (this) otrzymuje wartość P. Słowo kluczowe this oznacza obiekt bieżący: ten, w którym znajduje się wykonywana metoda. Skąd to wiemy? Przyjrzyjmy się, jak przebiega inicjalizacja obiektu, do którego odwołuje się p1 w programie wywołującym:
Wywoływana jest metoda initialise obiektu p1. Kiedy w tej metodzie odwołujemy się do obiektu this, w rzeczywistości odwołujemy się do obiektu p1. Metoda initialise mogłaby również zostać zapisana w następujący sposób:
public void initialise(String P, String N, int age){
prenom=P;
nom=N;
this.age=age;
}
Gdy metoda obiektu odwołuje się do atrybutu A tego obiektu, zapis this.A jest domyślny. Należy go użyć jawnie w przypadku konfliktu identyfikatorów. Dotyczy to następującej instrukcji:
this.age=age;
gdzie age oznacza atrybut bieżącego obiektu, a także parametr age przekazany do metody. Należy wówczas usunąć niejednoznaczność, oznaczając atrybut age jako this.age.
3.1.6. Program testowy
Oto program testowy:
public class test1{
public static void main(String arg[]){
personne p1=new personne();
p1.initialise("Jean","Dupont",30);
p1.identifie();
}
}
Klasa personne jest zdefiniowana w pliku źródłowym personne.java i została skompilowana:
E:\data\serge\JAVA\BASES\OBJETS\2>javac personne.java
E:\data\serge\JAVA\BASES\OBJETS\2>dir
10/06/2002 09:21 473 personne.java
10/06/2002 09:22 835 personne.class
10/06/2002 09:23 165 test1.java
To samo robimy w przypadku programu testowego:
E:\data\serge\JAVA\BASES\OBJETS\2>javac test1.java
E:\data\serge\JAVA\BASES\OBJETS\2>dir
10/06/2002 09:21 473 personne.java
10/06/2002 09:22 835 personne.class
10/06/2002 09:23 165 test1.java
10/06/2002 09:25 418 test1.class
Można się dziwić, że program test1.java nie importuje klasy personne za pomocą instrukcji:
Gdy kompilator napotka w kodzie źródłowym odwołanie do klasy, która nie została zdefiniowana w tym samym pliku źródłowym, szuka tej klasy w różnych miejscach:
- w pakietach zaimportowanych za pomocą instrukcji import
- w katalogu, z którego uruchomiono kompilator
W naszym przykładzie kompilator został uruchomiony z katalogu zawierającego plik personne.class, co wyjaśnia, dlaczego znalazł definicję klasy personne. Umieszczenie w tej sytuacji instrukcji import powoduje błąd kompilacji:
E:\data\serge\JAVA\BASES\OBJETS\2>javac test1.java
test1.java:1: '.' expected
import personne;
^
1 error
Aby uniknąć tego błędu, ale jednocześnie przypomnieć, że należy zaimportować klasę „person”, w przyszłości na początku programu należy wpisać:
Teraz możemy uruchomić plik test1.class:
W jednym pliku źródłowym można połączyć kilka klas. Połączmy zatem klasy personne i test1 w pliku źródłowym test2.java. Klasa test1 została przemianowana na test2, aby uwzględnić zmianę nazwy pliku źródłowego:
// importowane pakiety
import java.io.*;
class personne{
// atrybuty
private String prenom; // imię mojej osoby
private String nom; // jej nazwisko
private int age; // jej wiek
// metoda
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}//inicjuje
// metoda
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}//identyfikuje
}//klasa
public class test2{
public static void main(String arg[]){
personne p1=new personne();
p1.initialise("Jean","Dupont",30);
p1.identifie();
}
}
Należy zauważyć, że klasa personne nie posiada już atrybutu public. W rzeczywistości w pliku źródłowym Java tylko jedna klasa może posiadać atrybut public. Jest to ta, która posiada funkcję main. Ponadto plik źródłowy musi nosić nazwę tej ostatniej. Skompilujmy plik test2.java:
E:\data\serge\JAVA\BASES\OBJETS\3>dir
10/06/2002 09:36 633 test2.java
E:\data\serge\JAVA\BASES\OBJETS\3>javac test2.java
E:\data\serge\JAVA\BASES\OBJETS\3>dir
10/06/2002 09:36 633 test2.java
10/06/2002 09:41 832 personne.class
10/06/2002 09:41 418 test2.class
Warto zauważyć, że dla każdej klasy obecnej w pliku źródłowym wygenerowano plik .class. Uruchommy teraz plik test2.class:
W dalszej kolejności będziemy stosować obie metody zamiennie:
- klasy zebrane w jednym pliku źródłowym
- jedna klasa na plik źródłowy
3.1.7. Inna metoda inicjuje
Rozważmy ponownie klasę personne i dodajmy do niej następującą metodę:
public void initialise(personne P){
prenom=P.prenom;
nom=P.nom;
this.age=P.age;
}
Mamy teraz dwie metody o nazwie initialise: jest to dopuszczalne, o ile przyjmują one różne parametry. Tak właśnie jest w tym przypadku. Parametrem jest teraz odwołanie P do osoby. Atrybuty osoby P są następnie przypisywane do bieżącego obiektu (this). Warto zauważyć, że metoda initialise ma bezpośredni dostęp do atrybutów obiektu P, mimo że są one typu private. Zasada ta zawsze obowiązuje: metody obiektu O1 należącego do klasy C zawsze mają dostęp do atrybutów prywatnych innych obiektów tej samej klasy C.
Oto test nowej klasy personne:
// import person;
import java.io.*;
public class test1{
public static void main(String arg[]){
personne p1=new personne();
p1.initialise("Jean","Dupont",30);
System.out.print("p1=");
p1.identifie();
personne p2=new personne();
p2.initialise(p1);
System.out.print("p2=");
p2.identifie();
}
}
oraz wyniki:
3.1.8. Konstruktory klasy „osoba”
Konstruktor to metoda o nazwie zgodnej z nazwą klasy, wywoływana podczas tworzenia obiektu. Zazwyczaj służy ona do zainicjowania obiektu. Jest to metoda, która może przyjmować argumenty, ale nie zwraca żadnego wyniku. Jej prototyp lub definicja nie są poprzedzone żadnym typem (nawet void).
Jeśli klasa posiada konstruktor przyjmujący n argumentów argi, deklaracja i inicjalizacja obiektu tej klasy może odbywać się w następującej postaci:
classe objet =new classe(arg1,arg2, ... argn);
lub
classe objet;
…
objet=new classe(arg1,arg2, ... argn);
Gdy klasa posiada jeden lub więcej konstruktorów, do utworzenia obiektu tej klasy należy obowiązkowo użyć jednego z tych konstruktorów. Jeśli klasa C nie posiada żadnego konstruktora, ma ona domyślny konstruktor bez parametrów: public C(). Atrybuty obiektu są wówczas inicjowane wartościami domyślnymi. Tak właśnie było w przypadku, gdy w poprzednich programach napisaliśmy:
Stwórzmy dwa konstruktory dla naszej klasy personne:
public class personne{
// atrybuty
private String prenom;
private String nom;
private int age;
// konstruktory
public personne(String P, String N, int age){
initialise(P,N,age);
}
public personne(personne P){
initialise(P);
}
// metoda
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
public void initialise(personne P){
this.prenom=P.prenom;
this.nom=P.nom;
this.age=P.age;
}
// metoda
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
Nasi dwaj konstruktorzy ograniczają się do wywołania odpowiednich metod initialise. Przypominamy, że gdy w konstruktorze pojawia się na przykład zapis initialise(P), kompilator przekształca go na this.initialise(P). W konstruktorze wywoływana jest zatem metoda initialise w celu przetworzenia obiektu, do którego odwołuje się this, czyli obiektu bieżącego, który jest właśnie tworzony.
Oto program testowy:
// import osoba;
import java.io.*;
public class test1{
public static void main(String arg[]){
personne p1=new personne("Jean","Dupont",30);
System.out.print("p1=");
p1.identifie();
personne p2=new personne(p1);
System.out.print("p2=");
p2.identifie();
}
}
oraz uzyskane wyniki:
3.1.9. Odwołania do obiektów
Zawsze używamy tej samej klasy personne. Program testowy wygląda następująco:
// import osoba;
import java.io.*;
public class test1{
public static void main(String arg[]){
// p1
personne p1=new personne("Jean","Dupont",30);
System.out.print("p1="); p1.identifie();
// p2 odwołuje się do tego samego obiektu co p1
personne p2=p1;
System.out.print("p2="); p2.identifie();
// p3 odwołuje się do obiektu, który będzie kopią obiektu, do którego odwołuje się p1
personne p3=new personne(p1);
System.out.print("p3="); p3.identifie();
// zmieniamy stan obiektu, do którego odwołuje się p1
p1.initialise("Micheline","Benoît",67);
System.out.print("p1="); p1.identifie();
// ponieważ p2 = p1, obiekt, do którego odwołuje się p2, musiał zmienić swój stan
System.out.print("p2="); p2.identifie();
// ponieważ p3 nie odwołuje się do tego samego obiektu co p1, obiekt, do którego odwołuje się p3, nie musiał ulec zmianie
System.out.print("p3="); p3.identifie();
}
}
Otrzymane wyniki są następujące:
p1=Jean,Dupont,30
p2=Jean,Dupont,30
p3=Jean,Dupont,30
p1=Micheline,Benoît,67
p2=Micheline,Benoît,67
p3=Jean,Dupont,30
Gdy zadeklaruje się zmienną p1 za pomocą
p1 odwołuje się do obiektu personne("Jean","Dupont",30), ale nie jest samym obiektem. W języku C można by powiedzieć, że jest to wskaźnik, c.a.d, wskazujący adres utworzonego obiektu. Jeśli następnie wpiszemy:
To nie obiekt personne("Jean","Dupont",30) ulega zmianie, lecz wartość odwołania p1. Obiekt osoba("Jean","Dupont",30) zostanie „utracony”, jeśli nie będzie odwołany przez żadną inną zmienną.
Gdy piszemy:
inicjujemy wskaźnik p2: „wskazuje” on na ten sam obiekt (oznacza ten sam obiekt), co wskaźnik p1. Zatem jeśli zmodyfikujemy obiekt, na który „wskazuje” (lub do którego odwołuje się) wskaźnik p1, zmodyfikujemy również obiekt, do którego odwołuje się wskaźnik p2.
Gdy zapisujemy:
tworzony jest nowy obiekt, będący kopią obiektu, do którego odwołuje się p1. Ten nowy obiekt będzie odwołany przez p3. Jeśli zmodyfikujemy obiekt, na który „wskazuje” (lub do którego odwołuje się) p1, nie wpłynie to w żaden sposób na obiekt, do którego odwołuje się p3. Potwierdzają to uzyskane wyniki.
3.1.10. Obiekty tymczasowe
W wyrażeniu można jawnie wywołać konstruktor obiektu: obiekt ten jest tworzony, ale nie mamy do niego dostępu (na przykład w celu jego modyfikacji). Ten obiekt tymczasowy jest tworzony na potrzeby obliczenia wyrażenia, a następnie porzucany. Zajmowana przez niego przestrzeń pamięci zostanie później automatycznie odzyskana przez program zwany „źródłem zbierania śmieci”, którego zadaniem jest odzyskiwanie przestrzeni pamięci zajmowanej przez obiekty, do których dane programu już nie odwołują się.
Rozważmy następujący przykład:
// import person;
public class test1{
public static void main(String arg[]){
new personne(new personne("Jean","Dupont",30)).identifie();
}
}
i zmodyfikujmy konstruktory klasy personne tak, aby wyświetlały komunikat:
// producenci
public personne(String P, String N, int age){
System.out.println("Constructeur personne(String, String, int)");
initialise(P,N,age);
}
public personne(personne P){
System.out.println("Constructeur personne(personne)");
initialise(P);
}
Otrzymujemy następujące wyniki:
ilustrujące sukcesywne tworzenie dwóch obiektów tymczasowych.
3.1.11. Metody odczytu i zapisu atrybutów prywatnych
Dodajemy do klasy personne metody niezbędne do odczytu lub modyfikacji stanu atrybutów obiektów:
public class personne{
private String prenom;
private String nom;
private int age;
public personne(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
public personne(personne P){
this.prenom=P.prenom;
this.nom=P.nom;
this.age=P.age;
}
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
// dostawcy
public String getPrenom(){
return prenom;
}
public String getNom(){
return nom;
}
public int getAge(){
return age;
}
//modyfikatorzy
public void setPrenom(String P){
this.prenom=P;
}
public void setNom(String N){
this.nom=N;
}
public void setAge(int age){
this.age=age;
}
}
Testujemy nową klasę za pomocą następującego programu:
// import osób;
public class test1{
public static void main(String[] arg){
personne P=new personne("Jean","Michelin",34);
System.out.println("P=("+P.getPrenom()+","+P.getNom()+","+P.getAge()+")");
P.setAge(56);
System.out.println("P=("+P.getPrenom()+","+P.getNom()+","+P.getAge()+")");
}
}
i otrzymujemy następujące wyniki:
3.1.12. Metody i atrybuty klasy
Załóżmy, że chcemy policzyć liczbę obiektów personne utworzonych w aplikacji. Można samodzielnie zarządzać licznikiem, ale istnieje ryzyko pominięcia obiektów tymczasowych, które są tworzone tu i ówdzie. Bezpieczniejszym rozwiązaniem wydaje się umieszczenie w konstruktorach klasy personne instrukcji zwiększającej licznik. Problem polega na przekazaniu odwołania do tego licznika, aby konstruktor mógł go zwiększyć: należy przekazać im nowy parametr. Można również umieścić licznik w definicji klasy. Ponieważ jest to atrybut samej klasy, a nie konkretnego obiektu tej klasy, deklaruje się go inaczej za pomocą słowa kluczowego static:
Aby do niego odwołać się, piszemy personne.nbPersonnes, aby pokazać, że jest to atrybut samej klasy personne. W tym przypadku utworzyliśmy atrybut prywatny, do którego nie będziemy mieli bezpośredniego dostępu spoza klasy. Tworzymy zatem metodę publiczną, aby zapewnić dostęp do atrybutu klasy nbPersonnes. Aby uzyskać wartość nbPersonnes, metoda nie potrzebuje konkretnego obiektu: w rzeczywistości nbPersonnes nie jest atrybutem konkretnego obiektu, lecz atrybutem całej klasy. Potrzebna jest zatem metoda klasy, również zadeklarowana jako static:
która z zewnątrz będzie wywoływana za pomocą składni personne.getNbPersonnes(). Oto przykład.
Klasa personne przyjmuje następujący kształt:
public class personne{
// atrybut klasy
private static long nbPersonnes=0;
// atrybuty obiektów
…
// konstruktory
public personne(String P, String N, int age){
initialise(P,N,age);
nbPersonnes++;
}
public personne(personne P){
initialise(P);
nbPersonnes++;
}
// metoda
…
// metoda klasy
public static long getNbPersonnes(){
return nbPersonnes;
}
}// klasa
Z wykorzystaniem następującego programu:
// import person;
public class test1{
public static void main(String arg[]){
personne p1=new personne("Jean","Dupont",30);
personne p2=new personne(p1);
new personne(p1);
System.out.println("Nombre de personnes créées : "+personne.getNbPersonnes());
}// main
}//test1
otrzymujemy następujące wyniki:
3.1.13. Przekazywanie obiektu do funkcji
Jak już wspomnieliśmy, w Javie rzeczywiste parametry funkcji są przekazywane przez wartość: wartości parametrów rzeczywistych są kopiowane do parametrów formalnych. Funkcja nie może zatem modyfikować parametrów rzeczywistych.
W przypadku obiektu nie należy dać się zwieść nadużyciu językowemu, które ma miejsce systematycznie, gdy mówi się o „obiekcie” zamiast o „odwołaniu do obiektu”. Obiektem można manipulować wyłącznie za pośrednictwem odwołania (wskaźnika) do niego. To, co jest przekazywane do funkcji, nie jest więc samym obiektem, ale odwołaniem do tego obiektu. W parametrze formalnym kopiowana jest zatem wartość odwołania, a nie wartość samego obiektu: nie dochodzi do utworzenia nowego obiektu.
Jeśli odwołanie do obiektu R1 zostanie przekazane do funkcji, zostanie ono skopiowane do odpowiedniego parametru formalnego R2. W związku z tym odniesienia R2 i R1 wskazują ten sam obiekt. Jeśli funkcja modyfikuje obiekt, na który wskazuje R2, to oczywiście modyfikuje również obiekt, do którego odnosi się R1, ponieważ jest to ten sam obiekt.

Widać to na poniższym przykładzie:
// import Person;
public class test1{
public static void main(String arg[]){
personne p1=new personne("Jean","Dupont",30);
System.out.print("Paramètre effectif avant modification : ");
p1.identifie();
modifie(p1);
System.out.print("Paramètre effectif après modification : ");
p1.identifie();
}// main
private static void modifie(personne P){
System.out.print("Paramètre formel avant modification : ");
P.identifie();
P.initialise("Sylvie","Vartan",52);
System.out.print("Paramètre formel après modification : ");
P.identifie();
}// modyfikuje
}// klasa
Metoda modifie jest deklarowana jako static, ponieważ jest to metoda klasy: nie trzeba przed nią umieszczać prefiksu obiektu, aby ją wywołać. Uzyskane wyniki są następujące:
Constructeur personne(String, String, int)
Paramètre effectif avant modification : Jean,Dupont,30
Paramètre formel avant modification : Jean,Dupont,30
Paramètre formel après modification : Sylvie,Vartan,52
Paramètre effectif après modification : Sylvie,Vartan,52
Widać, że utworzono tylko jeden obiekt: obiekt osoby p1 z funkcji main oraz że obiekt ten został rzeczywiście zmodyfikowany przez funkcję modifie.
3.1.14. Zakapsułkowanie parametrów wyjściowych funkcji w obiekcie
Ze względu na przekazywanie parametrów przez wartość nie da się w Javie napisać funkcji, która miałaby parametry wyjściowe typu int, ponieważ nie ma możliwości przekazania odwołania do typu int, który nie jest obiektem. Można zatem utworzyć klasę hermetyzującą typ int:
public class entieres{
private int valeur;
public entieres(int valeur){
this.valeur=valeur;
}
public void setValue(int valeur){
this.valeur=valeur;
}
public int getValue(){
return valeur;
}
}
Poprzednia klasa posiada konstruktor umożliwiający zainicjowanie liczby całkowitej oraz dwie metody służące do odczytu i modyfikacji wartości tej liczby. Klasę tę testujemy za pomocą następującego programu:
// import typu integer;
public class test2{
public static void main(String[] arg){
entieres I=new entieres(12);
System.out.println("I="+I.getValue());
change(I);
System.out.println("I="+I.getValue());
}
private static void change(entieres entier){
entier.setValue(15);
}
}
i otrzymujemy następujące wyniki:
3.1.15. Tablica osób
Obiekt jest danymi takimi jak inne, dlatego też wiele obiektów można zgrupować w tablicy:
// import person;
public class test1{
public static void main(String arg[]){
personne[] amis=new personne[3];
System.out.println("----------------");
amis[0]=new personne("Jean","Dupont",30);
amis[1]=new personne("Sylvie","Vartan",52);
amis[2]=new personne("Neil","Armstrong",66);
int i;
for(i=0;i<amis.length;i++)
amis[i].identifie();
}
}
Instrukcja osoba[] przyjaciele = new osoba[3]; tworzy tablicę zawierającą 3 elementy typu personne. Te 3 elementy są tutaj zainicjowane wartościami null oraz c.a.d, co oznacza, że nie odwołują się one do żadnego obiektu. Ponownie, w wyniku nadużycia językowego, mówi się o tablicy obiektów, podczas gdy jest to jedynie tablica odwołań do obiektów. Utworzenie tablicy obiektów – która sama w sobie jest obiektem (obecność new) – nie powoduje zatem samo w sobie utworzenia żadnego obiektu typu jej elementów: należy to zrobić później.
Otrzymujemy następujące wyniki:
----------------
Constructeur personne(String, String, int)
Constructeur personne(String, String, int)
Constructeur personne(String, String, int)
Jean,Dupont,30
Sylvie,Vartan,52
Neil,Armstrong,66
3.2. Dziedzictwo poprzez przykład
3.2.1. Informacje ogólne
W tym miejscu omówimy pojęcie dziedziczenia. Celem dziedziczenia jest „dostosowanie” istniejącej klasy tak, aby spełniała nasze potrzeby. Załóżmy, że chcemy utworzyć klasę enseignant: nauczyciel jest osobą szczególną. Posiada on atrybuty, których nie ma żadna inna osoba: na przykład przedmiot, którego naucza. Posiada jednak również atrybuty charakterystyczne dla każdej osoby: imię, nazwisko i wiek. Nauczyciel w pełni należy zatem do klasy personne, ale posiada dodatkowe atrybuty. Zamiast tworzyć klasę enseignant od podstaw, lepiej byłoby wykorzystać istniejące elementy klasy personne i dostosować je do specyficznego charakteru nauczycieli. Umożliwia nam to koncepcja dziedziczenia.
Aby wyrazić, że klasa enseignant dziedziczy właściwości klasy personne, zapiszemy:
public class enseignant extends personne
Klasa personne nazywana jest klasą nadrzędną (lub macierzystą), a klasa enseignant – klasą pochodną (lub potomną). Obiekt enseignant posiada wszystkie cechy obiektu personne: ma te same atrybuty i te same metody. Atrybuty i metody klasy nadrzędnej nie są powtarzane w definicji klasy podrzędnej: wystarczy wskazać atrybuty i metody dodane przez klasę podrzędną:
class enseignant extends personne{
// atrybuty
private int section;
// konstruktor
public enseignant(String P, String N, int age,int section){
super(P,N,age);
this.section=section;
}
}
Zakładamy, że klasa personne jest zdefiniowana w następujący sposób:
public class personne{
private String prenom;
private String nom;
private int age;
public personne(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
public personne(personne P){
this.prenom=P.prenom;
this.nom=P.nom;
this.age=P.age;
}
public String identite(){
return "personne("+prenom+","+nom+","+age+")";
}
// metody dostępu
public String getPrenom(){
return prenom;
}
public String getNom(){
return nom;
}
public int getAge(){
return age;
}
//modyfikatory
public void setPrenom(String P){
this.prenom=P;
}
public void setNom(String N){
this.nom=N;
}
public void setAge(int age){
this.age=age;
}
}
Metoda identifie została nieznacznie zmodyfikowana w celu zwrócenia ciągu znaków identyfikującego daną osobę i nosi teraz nazwę identite. W tym przypadku klasa enseignant uzupełnia metody i atrybuty klasy personne o:
- atrybut section, który jest numerem sekcji, do której należy nauczyciel w gronie pedagogicznym (ogólnie rzecz biorąc, jedna sekcja na jeden przedmiot)
- nowy konstruktor umożliwiający zainicjowanie wszystkich atrybutów nauczyciela
3.2.2. Tworzenie obiektu nauczyciela
Konstruktor klasy enseignant ma następującą postać:
// konstruktor
public enseignant(String P, String N, int age,int section){
super(P,N,age);
this.section=section;
}
Instrukcja super(P,N,age) stanowi wywołanie konstruktora klasy nadrzędnej, w tym przypadku klasy personne. Wiadomo, że ten konstruktor inicjuje pola „prenom”, „nom” oraz „age” obiektu personne zawartego w obiekcie étudiant. Wydaje się to dość skomplikowane i można by wolać zapisać to w następujący sposób:
// konstruktor
public enseignant(String P, String N, int age,int section){
this.prenom=P;
this.nom=N
this.age=age
this.section=section;
}
To niemożliwe. Klasa personne zadeklarowała jako prywatne (private) swoje trzy pola: prenom, nom i age. Tylko obiekty tej samej klasy mają bezpośredni dostęp do tych pól. Wszystkie pozostałe obiekty, w tym obiekty potomne, jak w tym przypadku, muszą korzystać z metod publicznych, aby uzyskać do nich dostęp. Sytuacja wyglądałaby inaczej, gdyby klasa personne zadeklarowała te trzy pola jako chronione (protected): wówczas zezwalałaby klasom pochodnym na bezpośredni dostęp do tych trzech pól. W naszym przykładzie użycie konstruktora klasy nadrzędnej było zatem właściwym rozwiązaniem i jest to standardowa procedura: podczas tworzenia obiektu potomnego najpierw wywołuje się konstruktor obiektu nadrzędnego, a następnie uzupełnia się inicjalizacje właściwe dla obiektu potomnego (w naszym przykładzie section).
Spróbujmy napisać pierwszy program:
// import osoby;
// import nauczyciel;
public class test1{
public static void main(String arg[]){
System.out.println(new enseignant("Jean","Dupont",30,27).identite());
}
}
Ten program ogranicza się do utworzenia obiektu enseignant (new) i jego identyfikacji. Klasa enseignant nie posiada metody identité, ale jej klasa nadrzędna posiada taką metodę, która ponadto jest publiczna: staje się ona poprzez dziedziczenie publiczną metodą klasy enseignant.
Pliki źródłowe klas są gromadzone w jednym katalogu, a następnie kompilowane:
E:\data\serge\JAVA\BASES\OBJETS\4>dir
10/06/2002 10:00 765 personne.java
10/06/2002 10:00 212 enseignant.java
10/06/2002 10:01 192 test1.java
E:\data\serge\JAVA\BASES\OBJETS\4>javac *.java
E:\data\serge\JAVA\BASES\OBJETS\4>dir
10/06/2002 10:00 765 personne.java
10/06/2002 10:00 212 enseignant.java
10/06/2002 10:01 192 test1.java
10/06/2002 10:02 316 enseignant.class
10/06/2002 10:02 1 146 personne.class
10/06/2002 10:02 550 test1.class
Uruchomiono plik test1.class:
3.2.3. Przedefiniowanie metody
W poprzednim przykładzie znaliśmy tożsamość nauczyciela z części personne, ale brakuje pewnych informacji dotyczących klasy enseignant (sekcji). Musimy więc napisać metodę pozwalającą zidentyfikować nauczyciela:
class enseignant extends personne{
int section;
public enseignant(String P, String N, int age,int section){
super(P,N,age);
this.section=section;
}
public String identite(){
return "enseignant("+super.identite()+","+section+")";
}
}
Metoda identite klasy enseignant opiera się na metodzie identite swojej klasy nadrzędnej (super.identite) w celu wyświetlenia swojej części „personne”, a następnie uzupełnia ją polem section, które jest specyficzne dla klasy enseignant.
Klasa enseignant posiada teraz dwie metody identite:
- jedną odziedziczoną po klasie nadrzędnej personne
- własną metodę
Jeśli E jest obiektem klasy enseignant, to E.identite oznacza metodę identite klasy enseignant. Mówi się, że metoda identite klasy nadrzędnej jest „przeładowana” przez metodę identite klasy podrzędnej. Ogólnie rzecz biorąc, jeśli O jest obiektem, a M metodą, to w celu wykonania metody O.M system szuka metody M w następującej kolejności:
- w klasie obiektu O
- w jej klasie nadrzędnej, jeśli taka istnieje
- w klasie nadrzędnej klasy nadrzędnej, jeśli taka istnieje
- itd…
Dziedziczenie pozwala zatem na nadpisanie w klasie potomnej metod o tej samej nazwie, które istnieją w klasie nadrzędnej. To właśnie umożliwia dostosowanie klasy potomnej do jej własnych potrzeb. W połączeniu z polimorfizmem, który omówimy nieco później, nadpisywanie metod stanowi główną zaletę dziedziczenia.
Rozważmy ten sam przykład, co poprzednio:
// import osoby;
// import nauczyciel;
public class test1{
public static void main(String arg[]){
System.out.println(new enseignant("Jean","Dupont",30,27).identite());
}
}
Tym razem uzyskane wyniki są następujące:
3.2.4. Polimorfizm
Rozważmy następującą linię klas: C0 C1 C2 … Cn
gdzie Ci Cj oznacza, że klasa Cj wywodzi się z klasy Ci. Oznacza to, że klasa Cj posiada wszystkie cechy klasy Ci oraz dodatkowe cechy. Niech Oi będą obiektami typu Ci. Dopuszczalne jest zapisanie:
W rzeczywistości, dzięki dziedziczeniu, klasa Cj posiada wszystkie cechy klasy Ci oraz dodatkowe. Zatem obiekt Oj typu Cj zawiera w sobie obiekt typu Ci. Operacja
sprawia, że Oi jest odwołaniem do obiektu typu Ci zawartego w obiekcie Oj.
Fakt, żezmienna Oi klasy Ci może w rzeczywistości odnosić się nie tylko do obiektu klasy Ci, ale do dowolnego obiektu pochodnego od klasy Ci, nazywa się polimorfizmem: zdolność zmiennej do odwoływania się do różnych typów obiektów.
Weźmy przykład i rozważmy następującą funkcję niezależną od jakiejkolwiek klasy:
Klasa Object jest „klasą nadrzędną” wszystkich klas Java. Zatem gdy piszemy:
to domyślnie zapisujemy:
W ten sposób każdy obiekt Java zawiera w sobie część typu Object. Można więc zapisać:
Formalny parametr typu Object funkcji affiche otrzyma wartość typu enseignant. Ponieważ nauczyciel dziedziczy po Object, jest to poprawne.
3.2.5. Przeciążanie i polimorfizm
Uzupełnijmy naszą funkcję affiche:
Metoda obj.toString() zwraca ciąg znaków identyfikujący obiekt obj w postaci nom_de_la_classe@adresse_de_l'obiekt. Co się dzieje w przypadku naszego poprzedniego przykładu:
System będzie musiał wykonać instrukcję System.out.println(e.toString()), gdzie e jest obiektem nadrzędnym. Szuka metody toString w hierarchii klas prowadzącej do klasy enseignant, zaczynając od ostatniej:
- w klasie enseignant nie znajduje metody toString()
- w klasie nadrzędnej personne nie znajduje metody toString()
- w klasie nadrzędnej Object znajduje metodę toString() i ją wykonuje
Pokazuje to poniższy program:
// import osoby;
// import nauczyciela;
public class test1{
public static void main(String arg[]){
enseignant e=new enseignant("Lucile","Dumas",56,61);
affiche(e);
personne p=new personne("Jean","Dupont",30);
affiche(p);
}
public static void affiche(Object obj){
System.out.println(obj.toString());
}
}
Otrzymane wyniki są następujące:
Czyli obiekt o nazwie nom_de_la_classe@adresse_de_l. Ponieważ nie jest to zbyt jednoznaczne, można ulec pokusie, by zdefiniować metodę toString dla klas personne i etudiant, która nadpisywałaby metodę toString klasy nadrzędnej Object. Zamiast pisać metody, które byłyby zbliżone do już istniejących metod identite w klasach personne i enseignant, wystarczy zmienić nazwy tych metod identite na toString:
public class personne{
...
public String toString(){
return "personne("+prenom+","+nom+","+age+")";
}
...
}
class enseignant extends personne{
int section;
…
public String toString(){
return "enseignant("+super.toString()+","+section+")";
}
}
Przy użyciu tego samego programu testowego, co poprzednio, uzyskano następujące wyniki:
3.3. Klasy wewnętrzne
Klasa może zawierać definicję innej klasy. Rozważmy następujący przykład:
// zaimportowane klasy
import java.io.*;
public class test1{
// klasa wewnętrzna
private class article{
// definiujemy strukturę
private String code;
private String nom;
private double prix;
private int stockActuel;
private int stockMinimum;
// konstruktor
public article(String code, String nom, double prix, int stockActuel, int stockMinimum){
// inicjalizacja atrybutów
this.code=code;
this.nom=nom;
this.prix=prix;
this.stockActuel=stockActuel;
this.stockMinimum=stockMinimum;
}//konstruktor
//toString
public String toString(){
return "article("+code+","+nom+","+prix+","+stockActuel+","+stockMinimum+")";
}//toString
}//klasa artykułu
// dane lokalne
private article art=null;
// producent
public test1(String code, String nom, double prix, int stockActuel, int stockMinimum){
// definicja atrybutu
art=new article(code, nom, prix, stockActuel,stockMinimum);
}//test1
// metoda dostępowa
public article getArticle(){
return art;
}//getArticle
public static void main(String arg[]){
// utworzenie instancji test1
test1 t1=new test1("a100","velo",1000,10,5);
// wyświetlanie test1.art
System.out.println("art="+t1.getArticle());
}//strona główna
}// koniec klasy
Klasa test1 zawiera definicję innej klasy, klasy article. Mówi się, że article jest klasą wewnętrzną klasy test1. Może to być przydatne, gdy klasa wewnętrzna ma zastosowanie wyłącznie w klasie, która ją zawiera. Podczas kompilacji powyższego kodu źródłowego test1.java otrzymujemy dwa pliki .class:
E:\data\serge\JAVA\classes\interne>dir
05/06/2002 17:26 1 362 test1.java
05/06/2002 17:26 941 test1$article.class
05/06/2002 17:26 1 020 test1.class
Plik test1$article.class został wygenerowany dla klasy article, będącej częścią klasy test1. Po uruchomieniu powyższego programu otrzymujemy następujące wyniki:
3.4. Interfejsy
Interfejs to zbiór prototypów metod lub właściwości, które tworzą umowę. Klasa, która decyduje się na implementację interfejsu, zobowiązuje się do zapewnienia implementacji wszystkich metod zdefiniowanych w interfejsie. Kompilator weryfikuje tę implementację.
Oto na przykład definicja interfejsu java.util.Enumeration:
Podsumowanie metod | ||
boolean | hasMoreElements() Sprawdza, czy ta wyliczalność zawiera więcej elementów. | |
Obiekt | nextElement() Zwraca następny element tej wyliczalności, jeśli ten obiekt wyliczalności ma co najmniej jeszcze jeden element do udostępnienia. | |
Każda klasa implementująca ten interfejs zostanie zadeklarowana jako
Metody hasMoreElements() i nextElement() muszą zostać zdefiniowane w klasie C.
Rozważmy poniższy kod definiujący klasę élève, która określa imię i nazwisko ucznia oraz jego ocenę z danego przedmiotu:
// klasa uczeń
public class élève{
// atrybuty publiczne
public String nom;
public double note;
// konstruktor
public élève(String NOM, double NOTE){
nom=NOM;
note=NOTE;
}//konstruktor
}//uczeń
Definiujemy klasę notes, która gromadzi oceny wszystkich uczniów z danego przedmiotu:
// klasy importowane
// import klasy uczeń
// klasa ocen
public class notes{
// atrybuty
protected String matière;
protected élève[] élèves;
// konstruktor
public notes (String MATIERE, élève[] ELEVES){
// zapisanie danych uczniów i przedmiotów
matière=MATIERE;
élèves=ELEVES;
}//oceny
// toString
public String toString(){
String valeur="matière="+matière +", notes=(";
int i;
// łączymy wszystkie oceny
for (i=0;i<élèves.length-1;i++){
valeur+="["+élèves[i].nom+","+élèves[i].note+"],";
};
//ostatnia ocena
if(élèves.length!=0){ valeur+="["+élèves[i].nom+","+élèves[i].note+"]";}
valeur+=")";
// koniec
return valeur;
}//toString
}//klasa
Atrybuty matière i élèves zostały zadeklarowane jako protected, aby były dostępne z klasy pochodnej. Postanawiamy utworzyć klasę pochodną notes z klasy notesStats, która będzie posiadała dwa dodatkowe atrybuty: średnią i odchylenie standardowe ocen:
public class notesStats extends notes implements Istats {
// atrybuty
private double _moyenne;
private double _écartType;
Klasa notesStats wywodzi się z klasy notes i implementuje następujący interfejs Istats:
// interfejs
public interface Istats{
double moyenne();
double écartType();
}//
Oznacza to, że klasa notesStats musi posiadać dwie metody o nazwach moyenne i écartType o sygnaturze określonej w interfejsie Istats. Klasa notesStats wygląda następująco:
// importowane klasy
// import notes;
// import Istats;
// import ucznia;
public class notesStats extends notes implements Istats {
// atrybuty
private double _moyenne;
private double _écartType;
// konstruktor
public notesStats (String MATIERE, élève[] ELEVES){
// tworzenie klasy nadrzędnej
super(MATIERE,ELEVES);
// obliczanie średniej ocen
double somme=0;
for (int i=0;i<élèves.length;i++){
somme+=élèves[i].note;
}
if(élèves.length!=0) _moyenne=somme/élèves.length;
else _moyenne=-1;
// odchylenie standardowe
double carrés=0;
for (int i=0;i<élèves.length;i++){
carrés+=Math.pow((élèves[i].note-_moyenne),2);
}//for
if(élèves.length!=0) _écartType=Math.sqrt(carrés/élèves.length);
else _écartType=-1;
}//konstruktor
// ToString
public String toString(){
return super.toString()+",moyenne="+_moyenne+",écart-type="+_écartType;
}//ToString
// metody interfejsu Istats
public double moyenne(){
// oblicza średnią ocen
return _moyenne;
}//średnia
public double écartType(){
// zwraca odchylenie standardowe
return _écartType;
}//écartType
}//klasa
Średnia _moyenne i odchylenie standardowe _ecartType są obliczane już podczas tworzenia obiektu. Dlatego metody moyenne i écartType muszą jedynie zwracać wartości atrybutów _moyenne i _ecartType. Obie metody zwracają -1, jeśli tablica uczniów jest pusta.
Następująca klasa testowa:
// zaimportowane klasy
// import ucznia;
// import Istats;
// import ocen;
// import notesStats;
// klasa testowa
public class test{
public static void main(String[] args){
// kilku uczniów i oceny
élève[] ELEVES=new élève[] { new élève("paul",14),new élève("nicole",16), new élève("jacques",18)};
// które zapisujemy w obiekcie „noty”
notes anglais=new notes("anglais",ELEVES);
// i wyświetla
System.out.println(""+anglais);
// to samo z średnią i odchyleniem standardowym
anglais=new notesStats("anglais",ELEVES);
System.out.println(""+anglais);
}//główna
}//klasa
daje następujące wyniki:
matière=anglais, notes=([paul,14.0],[nicole,16.0],[jacques,18.0])
matière=anglais, notes=([paul,14.0],[nicole,16.0],[jacques,18.0]),moyenne=16.0,écart-type=1.632993161855452
Każda z klas w tym przykładzie ma swój własny plik źródłowy:
E:\data\serge\JAVA\interfaces\notes>dir
06/06/2002 14:06 707 notes.java
06/06/2002 14:06 878 notes.class
06/06/2002 14:07 1 160 notesStats.java
06/06/2002 14:02 101 Istats.java
06/06/2002 14:02 138 Istats.class
06/06/2002 14:05 247 élève.java
06/06/2002 14:05 309 élève.class
06/06/2002 14:07 1 103 notesStats.class
06/06/2002 14:10 597 test.java
06/06/2002 14:10 931 test.class
Klasa notesStats mogłaby równie dobrze zaimplementować metody moyenne i écartType dla siebie samej, nie wskazując, że implementuje interfejs Istats. Jaki jest zatem sens istnienia interfejsów? Jest on następujący: funkcja może przyjmować jako parametr dane o typie interfejsu I. Każdy obiekt klasy C implementującej interfejs I będzie wówczas mógł być parametrem tej funkcji. Rozważmy następujący interfejs:
// interfejs Iexample
public interface Iexemple{
int ajouter(int i,int j);
int soustraire(int i,int j);
}//interfejs
Interfejs Iexemple definiuje dwie metody: ajouter i soustraire. Poniższe klasy classe1 i classe2 implementują ten interfejs.
// importowane klasy
// import Iprzykład;
public class classe1 implements Iexemple{
public int ajouter(int a, int b){
return a+b+10;
}
public int soustraire(int a, int b){
return a-b+20;
}
}//klasa
// zaimportowane klasy
// import Iexemple;
public class classe2 implements Iexemple{
public int ajouter(int a, int b){
return a+b+100;
}
public int soustraire(int a, int b){
return a-b+200;
}
}//klasa
Aby uprościć przykład, klasy te nie wykonują żadnych innych czynności poza implementacją interfejsu Iexemple. Rozważmy teraz następujący przykład:
// zaimportowane klasy
// import klasy1;
// import klasy2;
// klasa testowa
public class test{
// funkcja statyczna
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//obliczyć
// funkcja main
public static void main(String[] arg){
// utworzenie dwóch obiektów klasy1 i klasy2
classe1 c1=new classe1();
classe2 c2=new classe2();
// wywołania funkcji statycznej „oblicz”
calculer(4,3,c1);
calculer(14,13,c2);
}//main
}//klasa test
Funkcja statyczna calculer przyjmuje jako parametr element typu Iexemple. Może zatem otrzymać dla tego parametru zarówno obiekt typu classe1, jak i typu classe2. Tak właśnie postępuje się w funkcji main, uzyskując następujące wyniki:
Widać zatem, że mamy tu do czynienia z właściwością zbliżoną do polimorfizmu znanego z klas. Jeśli więc zbiór klas Ci, które nie są ze sobą powiązane poprzez dziedziczenie (a zatem nie można wykorzystać polimorfizmu dziedziczenia), posiada zbiór metod o tej samej sygnaturze, interesujące może być zgrupowanie tych metod w interfejsie I, z którego dziedziczyłyby wszystkie odnośne klasy. Instancje tych klas Ci mogą być wówczas wykorzystywane jako parametry funkcji przyjmujących parametr typu I, c.a.d. Są to funkcje wykorzystujące wyłącznie metody obiektów Ci zdefiniowane w interfejsie I, a nie konkretne atrybuty i metody poszczególnych klas Ci.
W poprzednim przykładzie każda klasa lub interfejs stanowił osobny plik źródłowy:
E:\data\serge\JAVA\interfaces\opérations>dir
06/06/2002 14:33 128 Iexemple.java
06/06/2002 14:34 218 classe1.java
06/06/2002 14:32 220 classe2.java
06/06/2002 14:33 144 Iexemple.class
06/06/2002 14:34 325 classe1.class
06/06/2002 14:34 326 classe2.class
06/06/2002 14:36 583 test.java
06/06/2002 14:36 628 test.class
Na koniec należy zauważyć, że dziedziczenie interfejsów może być wielokrotne, c.a.d. Można to zapisać jako
gdzie ij to interfejsy.
3.5. Klasy anonimowe
W poprzednim przykładzie klasy classe1 i classe2 nie musiałyby być definiowane jawnie. Rozważmy następujący program, który wykonuje zasadniczo to samo co poprzedni, ale bez jawnej definicji klas classe1 i classe2:
// zaimportowane klasy
// import Iexemple;
// klasa testowa
public class test2{
// klasa wewnętrzna
private static class classe3 implements Iexemple{
public int ajouter(int a, int b){
return a+b+1000;
}
public int soustraire(int a, int b){
return a-b+2000;
}
};//definicja klasy3
// funkcja statyczna
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//obliczyć
// funkcja main
public static void main(String[] arg){
// utworzenie dwóch obiektów implementujących interfejs Iexemple
Iexemple i1=new Iexemple(){
public int ajouter(int a, int b){
return a+b+10;
}
public int soustraire(int a, int b){
return a-b+20;
}
};//definicja i1
Iexemple i2=new Iexemple(){
public int ajouter(int a, int b){
return a+b+100;
}
public int soustraire(int a, int b){
return a-b+200;
}
};//definicja i2
// kolejny obiekt Iexemple
Iexemple i3=new classe3();
// wywołania statycznej funkcji „calculer”
calculer(4,3,i1);
calculer(14,13,i2);
calculer(24,23,i3);
}//main
}//klasa test
Cechą charakterystyczną jest kod:
// utworzenie dwóch obiektów implementujących interfejs Iexample
Iexemple i1=new Iexemple(){
public int ajouter(int a, int b){
return a+b+10;
}
public int soustraire(int a, int b){
return a-b+20;
}
};//definicja i1
Tworzymy obiekt i1, którego jedyną rolą jest implementacja interfejsu Iexemple. Obiekt ten jest typu Iexemple. Można zatem tworzyć obiekty typu interfejs. Bardzo wiele metod klas Java zwraca obiekty typu interfejs c.a.d – obiekty, których jedyną rolą jest implementacja metod interfejsu. Aby utworzyć obiekt i1, można by pokusić się o napisanie:
Iexemple i1=new Iexemple()
Jednak interfejsu nie można instancjonować. Instancjonować można jedynie klasę implementującą ten interfejs. W tym przypadku definiujemy taką klasę „na bieżąco” w samym treści definicji obiektu i1:
Iexemple i1=new Iexemple(){
public int ajouter(int a, int b){
// definicja funkcji „dodać”
}
public int soustraire(int a, int b){
// definicja „odejmować”
}
};//definicja i1
Znaczenie takiej instrukcji jest analogiczne do sekwencji:
public class test2{
................
// klasa wewnętrzna
private static class classe1 implements Iexemple{
public int ajouter(int a, int b){
// definicja dodawania
}
public int soustraire(int a, int b){
// definicja odejmowania
}
};//definicja klasy1
.................
public static void main(String[] arg){
...........
Iexemple i1=new classe1();
}//main
}//klasa
W powyższym przykładzie instancjonowana jest klasa, a nie interfejs. Klasa zdefiniowana „w locie” nazywana jest klasą anonimową. Jest to metoda często stosowana do instancjonowania obiektów, których jedyną rolą jest implementacja interfejsu.
Wykonanie powyższego programu daje następujące wyniki:
W powyższym przykładzie wykorzystano klasy anonimowe do implementacji interfejsu. Można je również wykorzystać do tworzenia klas pochodnych, które nie posiadają konstruktorów z parametrami. Rozważmy następujący przykład:
// importowane klasy
// import Iexemple;
class classe3 implements Iexemple{
public int ajouter(int a, int b){
return a+b+1000;
}
public int soustraire(int a, int b){
return a-b+2000;
}
};//definicja klasy3
public class test4{
// funkcja statyczna
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//obliczyć
// metoda main
public static void main(String args[]){
// definicja klasy anonimowej dziedziczącej po klasie3
// w celu ponownego zdefiniowania funkcji odejmowania
classe3 i1=new classe3(){
public int ajouter(int a, int b){
return a+b+10000;
}//odejmowanie
};//i1
// wywołania statycznej funkcji „calculer”
calculer(4,3,i1);
}//main
}//klasa
Znajdujemy tu klasę classe3, która implementuje interfejs Iexemple. W funkcji main definiujemy zmienną i1, której typem jest klasa pochodna od classe3. Ta klasa pochodna jest definiowana „w locie” w klasie anonimowej i redefiniuje metodę ajouter klasy classe3. Składnia jest identyczna jak w przypadku klasy anonimowej implementującej interfejs. Jednak w tym przypadku kompilator wykrywa, że classe3 nie jest interfejsem, lecz klasą. Dla kompilatora jest to zatem pochodna klasy. Wszystkie metody, które kompilator znajdzie w treści klasy anonimowej, zastąpią metody o tej samej nazwie w klasie bazowej.
Wykonanie powyższego programu daje następujące wyniki:
3.6. Pakiety
3.6.1. Tworzenie klas w pakiecie
Aby wyświetlić wiersz na ekranie, używamy instrukcji
Jeśli przyjrzymy się definicji klasy System, zauważymy, że w rzeczywistości nazywa się ona java.lang.System:

Sprawdźmy to na przykładzie:
public class test1{
public static void main(String[] args){
java.lang.System.out.println("Coucou");
}//main
}//klasa
Skompilujmy i uruchommy ten program:
E:\data\serge\JAVA\classes\paquetages>javac test1.java
E:\data\serge\JAVA\classes\paquetages>dir
06/06/2002 15:40 127 test1.java
06/06/2002 15:40 410 test1.class
E:\data\serge\JAVA\classes\paquetages>java test1
Coucou
Dlaczego więc możemy napisać
System.out.println("Coucou");
zamiast
java.lang.System.out.println("Coucou");
Ponieważ w przypadku każdego programu Java domyślnie następuje systematyczne importowanie „pakietu” java.lang. Wszystko przebiega więc tak, jakby na początku każdego programu znajdowała się instrukcja:
Co oznacza ta instrukcja? Zapewnia ona dostęp do wszystkich klas w pakiecie java.lang. Kompilator znajdzie w nim plik System.class definiujący klasę System. Nie wiemy jeszcze, gdzie kompilator znajdzie pakiet java.lang ani jak wygląda pakiet. Wrócimy do tego później. Aby utworzyć klasę w pakiecie, piszemy:
Dla przykładu utwórzmy w pakiecie naszą klasę personne, którą omówiliśmy wcześniej. Jako nazwę pakietu wybierzemy istia.st. Klasa personne przyjmuje postać:
// nazwa pakietu, w którym zostanie utworzona klasa osoba
package istia.st;
// klasa osoba
public class personne{
// nazwisko, imię, wiek
private String prenom;
private String nom;
private int age;
// konstruktor 1
public personne(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
// toString
public String toString(){
return "personne("+prenom+","+nom+","+age+")";
}
}//klasa
Ta klasa jest kompilowana, a następnie umieszczana w katalogu istia\st w bieżącym katalogu. Dlaczego istia\st? Ponieważ pakiet nosi nazwę istia.st.
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 16:04 <DIR> istia
E:\data\serge\JAVA\classes\paquetages\personne>dir istia
06/06/2002 16:04 <DIR> st
E:\data\serge\JAVA\classes\paquetages\personne>dir istia\st
06/06/2002 16:28 675 personne.class
Teraz wykorzystajmy klasę personne w pierwszej klasie testowej:
public class test{
public static void main(String[] args){
istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);
System.out.println("p1="+p1);
}//ręka
}//klasa testowa
Warto zauważyć, że klasa personne ma teraz przedrostek z nazwą swojego pakietu: istia.st. Gdzie kompilator znajdzie klasę istia.st.personne? Kompilator szuka potrzebnych mu klas w predefiniowanej liście katalogów oraz w drzewie katalogów rozgałęziającym się od bieżącego katalogu. W tym przypadku będzie szukał klasy istia.st.personne w pliku istia\st\personne.class. Dlatego umieściliśmy plik personne.class w katalogu istia\st. Skompilujmy, a następnie uruchommy program testowy:
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 16:06 246 test.java
06/06/2002 16:04 <DIR> istia
06/06/2002 16:06 738 test.class
E:\data\serge\JAVA\classes\paquetages\personne>java test
p1=personne(Jean,Dupont,20)
Aby uniknąć wpisywania
istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);
klasę istia.st.personne można zaimportować wraz z klauzulą import:
import istia.st.personne;
Możemy zatem zapisać
personne p1=new personne("Jean","Dupont",20);
a kompilator przetłumaczy to na
istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);
Program testowy wygląda zatem następująco:
// importowane przestrzenie nazw
import istia.st.personne;
public class test2{
public static void main(String[] args){
personne p1=new personne("Jean","Dupont",20);
System.out.println("p1="+p1);
}//main
}//klasa test2
Skompilujmy i uruchommy ten nowy program:
E:\data\serge\JAVA\classes\paquetages\personne>javac test2.java
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 16:06 246 test.java
06/06/2002 16:04 <DIR> istia
06/06/2002 16:06 738 test.class
06/06/2002 16:47 236 test2.java
06/06/2002 16:50 740 test2.class
E:\data\serge\JAVA\classes\paquetages\personne>java test2
p1=personne(Jean,Dupont,20)
Umieściliśmy pakiet istia.st w bieżącym katalogu. Nie jest to konieczne. Umieśćmy go w folderze o nazwie mesClasses, również w bieżącym katalogu. Przypomnijmy, że klasy z pakietu istia.st znajdują się w folderze istia\st. Struktura katalogów w bieżącym katalogu wygląda następująco:
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 16:06 246 test.java
06/06/2002 16:06 738 test.class
06/06/2002 16:47 236 test2.java
06/06/2002 16:50 740 test2.class
06/06/2002 16:21 <DIR> mesClasses
E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses
06/06/2002 16:22 <DIR> istia
E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses\istia
06/06/2002 16:22 <DIR> st
E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses\istia\st
06/06/2002 16:01 1 153 personne.class
Teraz skompilujmy ponownie program test2.java:
E:\data\serge\JAVA\classes\paquetages\personne>javac test2.java
test2.java:2: package istia.st does not exist
import istia.st.personne;
Kompilator nie może już znaleźć pakietu istia.st, ponieważ został on przeniesiony. Należy zauważyć, że kompilator szuka go z powodu instrukcji import. Domyślnie szuka go w bieżącym katalogu, w folderze o nazwie istia\st, który już nie istnieje. Przyjrzyjmy się opcjom kompilatora:
E:\data\serge\JAVA\classes\paquetages\personne>javac
Usage: javac <options> <source files>
where possible options include:
-g Generate all debugging info
-g:none Generate no debugging info
-g:{lines,vars,source} Generate only some debugging info
-O Optimize; may hinder debugging or enlarge class file
-nowarn Generate no warnings
-verbose Output messages about what the compiler is doing
-deprecation Output source locations where deprecated APIs are used
-classpath <path> Specify where to find user class files
-sourcepath <path> Specify where to find input source files
-bootclasspath <path> Override location of bootstrap class files
-extdirs <dirs> Override location of installed extensions
-d <directory> Specify where to place generated class files
-encoding <encoding> Specify character encoding used by source files
-source <release> Provide source compatibility with specified release
-target <release> Generate class files for specific VM version
-help Print a synopsis of standard options
W tym przypadku przydatna może być opcja -classpath. Pozwala ona wskazać kompilatorowi, gdzie ma szukać klas i pakietów. Spróbujmy. Skompilujmy kod, informując kompilator, że pakiet istia.st znajduje się teraz w folderze mesClasses:
E:\data\serge\JAVA\classes\paquetages\personne>javac -classpath mesClasses test2.java
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:47 236 test2.java
06/06/2002 17:03 740 test2.class
06/06/2002 16:21 <DIR> mesClasses
Tym razem kompilacja przebiega bez problemów. Uruchommy program test2.class:
E:\data\serge\JAVA\classes\paquetages\personne>java test2
Exception in thread "main" java.lang.NoClassDefFoundError: istia/st/personne
at test2.main(test2.java:6)
Teraz kolej na maszynę wirtualną Java, która nie może znaleźć klasy istia/st/personne. Szuka jej w bieżącym katalogu, podczas gdy znajduje się ona obecnie w katalogu mesClasses. Przyjrzyjmy się opcjom maszyny wirtualnej Java:
E:\data\serge\JAVA\classes\paquetages\personne>java
Usage: java [-options] class [args...]
(to execute a class)
or java -jar [-options] jarfile [args...]
(to execute a jar file)
where options include:
-client to select the "client" VM
-server to select the "server" VM
-hotspot is a synonym for the "client" VM [deprecated]
The default VM is client.
-cp -classpath <directories and zip/jar files separated by ;>
set search path for application classes and resources
-D<name>=<value>
set a system property
-verbose[:class|gc|jni]
enable verbose output
-version print product version and exit
-showversion print product version and continue
-? -help print this help message
-X print help on non-standard options
-ea[:<packagename>...|:<classname>]
-enableassertions[:<packagename>...|:<classname>]
enable assertions
-da[:<packagename>...|:<classname>]
-disableassertions[:<packagename>...|:<classname>]
disable assertions
-esa | -enablesystemassertions
enable system assertions
-dsa | -disablesystemassertions
disable system assertions
Widać, że JVM posiada również opcję classpath, podobnie jak kompilator. Wykorzystajmy ją, aby wskazać lokalizację pakietu istia.st:
E:\data\serge\JAVA\classes\paquetages\personne>java.bat -classpath mesClasses test2
Exception in thread "main" java.lang.NoClassDefFoundError: test2
Nie posunęliśmy się zbyt daleko. Teraz to sama klasa test2 nie została znaleziona. Powód jest następujący: w przypadku braku słowa kluczowego classpath bieżący katalog jest systematycznie przeszukiwany podczas wyszukiwania klas, ale nie ma to miejsca, gdy słowo to jest obecne. W rezultacie klasa test2.class, która znajduje się w bieżącym katalogu, nie jest wykrywana. Jakie jest rozwiązanie? Należy dodać bieżący katalog do classpath. Bieżący katalog jest oznaczony symbolem .
E:\data\serge\JAVA\classes\paquetages\personne>java -classpath mesClasses;. test2
p1=personne(Jean,Dupont,20)
Skąd te wszystkie komplikacje? Celem pakietów jest uniknięcie konfliktów nazw między klasami. Rozważmy dwie firmy: E1 i E2, które dystrybuują klasy spakowane odpowiednio w pakietach com.e1 i com.e2. Niech klient C zakupi te dwa zestawy klas, w których obie firmy zdefiniowały klasę o nazwie personne. Klient C będzie odwoływał się do klasy personne firmyfirmy E1 jako com.e1.personne, a klasę firmy E2 jako com.e2.personne, unikając w ten sposób konfliktu nazw.
3.6.2. Wyszukiwanie pakietów
Kiedy piszemy w programie
, aby uzyskać dostęp do wszystkich klas pakietu java.util, to gdzie jest on lokalizowany? Wspomnieliśmy, że pakiety są domyślnie wyszukiwane w bieżącym katalogu lub na liście katalogów zadeklarowanych w opcji classpath kompilatora lub w JVM, jeśli ta opcja jest dostępna. Są one również wyszukiwane w katalogach lib w katalogu instalacyjnym JDK. Rozważmy ten katalog:

W tym przykładzie zostaną przeszukane drzewa katalogów jdk14\lib i jdk14\jre\lib w celu znalezienia w nich plików .class, .jar lub .zip, które są archiwami klas. Przeprowadźmy na przykład wyszukiwanie plików .jar znajdujących się w poprzednim katalogu jdk14:

Jest ich kilkadziesiąt. Plik .jar można otworzyć za pomocą narzędzia winzip. Otwórzmy powyższy plik rt.jar (rt=RunTime). Znajduje się w nim kilkaset plików .class, w tym te należące do pakietu java.util:

Prostym sposobem zarządzania pakietami jest umieszczenie ich w katalogu <jdk>\jre\lib, gdzie <jdk> jest katalogiem instalacyjnym pakietu JDK. Zazwyczaj pakiet zawiera kilka klas i wygodnie jest zebrać je w jednym pliku .jar (JAR = plik Java ARchive). Plik wykonywalny jar.exe znajduje się w folderze <jdk>\bin:
E:\data\serge\JAVA\classes\paquetages\personne>dir "e:\program files\jdk14\bin\jar.exe"
07/02/2002 12:52 28 752 jar.exe
Pomoc dotyczącą korzystania z programu jar można uzyskać, uruchamiając go bez parametrów:
E:\data\serge\JAVA\classes\paquetages\personne>"e:\program files\jdk14\bin\jar.exe"
Syntaxe : jar {ctxu}[vfm0M] [fichier-jar] [fichier-manifest] [rÚp -C] fichiers ...
Options :
-c crÚer un nouveau fichier d''archives
-t gÚnÚrer la table des matiÞres du fichier d''archives
-x extraire les fichiers nommÚs (ou tous les fichiers) du fichier d''archives
-u mettre Ó jour le fichier d''archives existant
-v gÚnÚrer des informations verbeuses sur la sortie standard
-f spÚcifier le nom du fichier d''archives
-m inclure les informations manifest provenant du fichier manifest spÚcifiÚ
-0 stocker seulement ; ne pas utiliser la compression ZIP
-M ne pas crÚer de fichier manifest pour les entrÚes
-i gÚnÚrer l''index pour les fichiers jar spÚcifiÚs
-C passer au rÚpertoire spÚcifiÚ et inclure le fichier suivant
Si un rÚpertoire est spÚcifiÚ, il est traitÚ rÚcursivement.
Les noms des fichiers manifest et d''archives doivent Ûtre spÚcifiÚs
dans l''ordre des indicateurs ''m'' et ''f''.
Exemple 1 : pour archiver deux fichiers de classe dans le fichier d''archives classes.jar :
jar cvf classes.jar Foo.class Bar.class
Exemple 2 : utilisez le fichier manifest existant ''monmanifest'' pour archiver tous les fichiers du
rÚpertoire foo/ dans ''classes.jar'':
jar cvfm classes.jar monmanifest -C foo/ .
Wróćmy do klasy personne.class utworzonej wcześniej w pakiecie istia.st:
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 17:36 195 test.java
06/06/2002 16:04 <DIR> istia
06/06/2002 16:06 738 test.class
06/06/2002 16:47 236 test2.java
06/06/2002 18:15 740 test2.class
E:\data\serge\JAVA\classes\paquetages\personne>dir istia
06/06/2002 16:04 <DIR> st
E:\data\serge\JAVA\classes\paquetages\personne>dir istia\st
06/06/2002 16:28 675 personne.class
Utwórzmy plik istia.st.jar zawierający wszystkie klasy z pakietu istia.st, a więc wszystkie klasy z powyższego drzewa istia\st:
E:\data\serge\JAVA\classes\paquetages\personne>"e:\program files\jdk14\bin\jar" cvf istia.st.jar istia\st\*
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 17:36 195 test.java
06/06/2002 16:04 <DIR> istia
06/06/2002 16:06 738 test.class
06/06/2002 16:47 236 test2.java
06/06/2002 18:15 740 test2.class
06/06/2002 18:08 874 istia.st.jar
Przyjrzyjmy się za pomocą pliku winzip zawartości pliku istia.st.jar:

Umieśćmy plik istia.st.jar w katalogu <jdk>\jre\lib\perso:
E:\data\serge\JAVA\classes\paquetages\personne>dir "e:\program files\jdk14\jre\lib\perso"
06/06/2002 18:08 874 istia.st.jar
Teraz skompilujmy program test2.java, a następnie uruchommy go:
E:\data\serge\JAVA\classes\paquetages\personne>javac -classpath istia.st.jar test2.java
E:\data\serge\JAVA\classes\paquetages\personne>java -classpath istia.st.jar;. test2
p1=personne(Jean,Dupont,20)
Zauważamy, że wystarczyło podać nazwę archiwum do przeszukania, bez konieczności wyraźnego wskazywania jego lokalizacji. Wszystkie katalogi w drzewie katalogów <jdk>\jre\lib są przeszukiwane w celu znalezienia żądanego pliku .jar.
3.7. Przykład IMPÔTS
Wracamy do obliczeń podatku omówionych już w poprzednim rozdziale i przetwarzamy je przy użyciu klasy. Przypomnijmy problem:
Rozważamy uproszczony przypadek podatnika, który ma do zadeklarowania wyłącznie swoje wynagrodzenie:
- obliczamy liczbę udziałów pracownika nbParts = nbEnfants/2 + 1, jeśli jest on osobą stanu wolnego, nbEnfants/2+2, jeśli jest żonaty, gdzie nbEnfants to liczba jego dzieci.
- jeśli ma co najmniej troje dzieci, otrzymuje dodatkową połowę udziału
- oblicza się jego dochód podlegający opodatkowaniu R = 0,72 * S, gdzie S to jego roczne wynagrodzenie
- oblicza się jego współczynnik rodzinny QF = R / nbParts
- oblicza się jego podatek I. Rozważmy następującą tabelę:
12620,0 | 0 | 0 |
13190 | 0,05 | 631 |
15640 | 0,1 | 1290,5 |
24 740 | 0,15 | 2072,5 |
31810 | 0,2 | 3309,5 |
39 970 | 0,25 | 4900 |
48360 | 0,3 | 6898,5 |
55790 | 0,35 | 9316,5 |
92970 | 0,4 | 12106 |
127860 | 0,45 | 16754,5 |
151250 | 0,50 | 23147,5 |
172 040 | 0,55 | 30710 |
195 000 | 0,60 | 39312 |
0 | 0,65 | 49062 |
Każdy wiersz zawiera 3 pola. Aby obliczyć podatek I, należy znaleźć pierwszy wiersz, w którym QF <= pole1. Na przykład, jeśli QF = 23000, znajdziemy wiersz
Podatek I wynosi wówczas 0,15*R – 2072,5*nbParts. Jeśli QF jest takie, że relacja QF <= pole1 nigdy nie jest spełniona, wówczas stosuje się współczynniki z ostatniego wiersza. W tym przypadku:
co daje podatek I = 0,65 * R – 49062 * nbParts.
Klasa impots zostanie zdefiniowana w następujący sposób:
// utworzenie klasy „podatki”
public class impots{
// dane niezbędne do obliczenia podatku
// pochodzą ze źródła zewnętrznego
private double[] limites, coeffR, coeffN;
// producent
public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
// sprawdzamy, czy wszystkie trzy tablice mają ten sam rozmiar
boolean OK=LIMITES.length==COEFFR.length && LIMITES.length==COEFFN.length;
if (! OK) throw new Exception ("Les 3 tableaux fournis n'ont pas la même taille("+
LIMITES.length+","+COEFFR.length+","+COEFFN.length+")");
// w porządku
this.limites=LIMITES;
this.coeffR=COEFFR;
this.coeffN=COEFFN;
}//producent
// obliczenie podatku
public long calculer(boolean marié, int nbEnfants, int salaire){
// obliczenie liczby udziałów
double nbParts;
if (marié) nbParts=(double)nbEnfants/2+2;
else nbParts=(double)nbEnfants/2+1;
if (nbEnfants>=3) nbParts+=0.5;
// obliczenie dochodu podlegającego opodatkowaniu i współczynnika rodzinnego
double revenu=0.72*salaire;
double QF=revenu/nbParts;
// obliczenie podatku
limites[limites.length-1]=QF+1;
int i=0;
while(QF>limites[i]) i++;
// zwrot wyniku
return (long)(revenu*coeffR[i]-nbParts*coeffN[i]);
}//obliczyć
}//klasa
Tworzony jest obiekt „impots” zawierający dane umożliwiające obliczenie podatku podatnika. Jest to stała część obiektu. Po utworzeniu tego obiektu można wielokrotnie wywoływać jego metodę „calculer”, która oblicza podatek podatnika na podstawie jego stanu cywilnego (żonaty lub nie), liczby dzieci oraz rocznego wynagrodzenia.
Program testowy mógłby wyglądać następująco:
//zaimportowane klasy
// import podatków;
import java.io.*;
public class test
{
public static void main(String[] arg) throws IOException
{
// interaktywny program do obliczania podatku
// użytkownik wprowadza trzy dane za pomocą klawiatury: żonaty nbEnfants wynagrodzenie
// następnie program wyświetla kwotę podatku do zapłaty
final String syntaxe="syntaxe : marié nbEnfants salaire\n"
+"marié : o pour marié, n pour non marié\n"
+"nbEnfants : nombre d'enfants\n"
+"salaire : salaire annuel en F";
// tabele danych niezbędnych do obliczenia podatku
double[] limites=new double[] {12620,13190,15640,24740,31810,39970,48360,55790,92970,127860,151250,172040,195000,0};
double[] coeffR=new double[] {0,0.05,0.1,0.15,0.2,0.25,0.3,0.35,0.4,0.45,0.5,0.55,0.6,0.65};
double[] coeffN=new double[] {0,631,1290.5,2072.5,3309.5,4900,6898.5,9316.5,12106,16754.5,23147.5,30710,39312,49062};
// utworzenie strumienia odczytu
BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
// utworzenie obiektu podatku
impots objImpôt=null;
try{
objImpôt=new impots(limites,coeffR,coeffN);
}catch (Exception ex){
System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
System.exit(1);
}//try-catch
// pętla nieskończona
while(true){
// żądanie parametrów obliczenia podatku
System.out.print("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :");
String paramètres=IN.readLine().trim();
// co należy zrobić?
if(paramètres==null || paramètres.equals("")) break;
// sprawdzanie liczby argumentów w wprowadzonej linii
String[] args=paramètres.split("\\s+");
int nbParamètres=args.length;
if (nbParamètres!=3){
System.err.println(syntaxe);
continue;
}//if
// sprawdzanie poprawności parametrów
// żonaty
String marié=args[0].toLowerCase();
if (! marié.equals("o") && ! marié.equals("n")){
System.err.println(syntaxe+"\nArgument marié incorrect : tapez o ou n");
continue;
}//if
// nbEnfants
int nbEnfants=0;
try{
nbEnfants=Integer.parseInt(args[1]);
if(nbEnfants<0) throw new Exception();
}catch (Exception ex){
System.err.println(syntaxe+"\nArgument nbEnfants incorrect : tapez un entier positif ou nul");
continue;
}//if
// wynagrodzenie
int salaire=0;
try{
salaire=Integer.parseInt(args[2]);
if(salaire<0) throw new Exception();
}catch (Exception ex){
System.err.println(syntaxe+"\nArgument salaire incorrect : tapez un entier positif ou nul");
continue;
}//if
// parametry są poprawne – obliczamy podatek
System.out.println("impôt="+objImpôt.calculer(marié.equals("o"),nbEnfants,salaire)+" F");
// kolejny podatnik
}//while
}//główna
}//klasa
Oto przykład działania poprzedniego programu:
E:\data\serge\MSNET\c#\impots\3>java test
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :q s d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument marié incorrect : tapez o ou n
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o s d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument nbEnfants incorrect : tapez un entier positif ou nul
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument salaire incorrect : tapez un entier positif ou nul
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :q s d f
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22504 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :