Skip to content

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

personne p1;
p1.prenom="Jean";
p1.nom="Dupont";
p1.age=30;

są niedozwolone. Musimy zainicjować obiekt typu personne za pomocą metody publicznej. Tą rolę pełni metoda initialise. Napiszemy:

personne p1;
p1.initialise("Jean","Dupont",30);

Zapis p1.initialise jest poprawny, ponieważ initialise jest publicznie dostępny.

3.1.4. Operator new

Sekwencja instrukcji

personne p1;
p1.initialise("Jean","Dupont",30);

jest nieprawidłowa. Instrukcja

    personne p1;

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:

personne p1=null;

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

p1.initialise("Jean","Dupont",30);

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ć:

personne p1=new personne();

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

p1.initialise("Jean","Dupont",30);

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:

p1.initialise("Jean","Dupont",30);

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:

import personne;

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ć:

// importowane klasy
// import person;

Teraz możemy uruchomić plik test1.class:

E:\data\serge\JAVA\BASES\OBJETS\2>java test1
Jean,Dupont,30

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:

E:\data\serge\JAVA\BASES\OBJETS\2>java test2
Jean,Dupont,30

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:

p1=Jean,Dupont,30
p2=Jean,Dupont,30

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:

    personne p1;
    p1=new personne();

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:

p1=Jean,Dupont,30
p2=Jean,Dupont,30

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ą

personne p1=new personne("Jean","Dupont",30);

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:

    p1=null

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:

personne p2=p1;

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:

personne p3=new personne(p1);

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:

Constructeur personne(String, String, int)
Constructeur personne(personne)
Jean,Dupont,30

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:

P=(Jean,Michelin,34)
P=(Jean,Michelin,56)

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:

    private static long nbPersonnes;        // liczba utworzonych osób

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:

public static long getNbPersonnes(){
    return nbPersonnes;
}

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:

    Nombre de personnes créées : 3

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.

Image

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:

I=12
I=15

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:

E:\data\serge\JAVA\BASES\OBJETS\4>java test1
personne(Jean,Dupont,30)

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:

    enseignant(personne(Jean,Dupont,30),27)

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:

    Oi=Oj avec j>i

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

    Oi=Oj

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:

    public static void affiche(Object obj){
        ….
    }

Klasa Object jest „klasą nadrzędną” wszystkich klas Java. Zatem gdy piszemy:

    public class personne

to domyślnie zapisujemy:

    public class personne extends Object

W ten sposób każdy obiekt Java zawiera w sobie część typu Object. Można więc zapisać:

    enseignant e;
    affiche(e);

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:

    public static void affiche(Object obj){
        System.out.println(obj.toString());
    }

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:

    enseignant e=new enseignant(...);
    affiche(e);

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:

enseignant@1ee789
personne@1ee770

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:

enseignant(personne(Lucile,Dumas,56),61)
personne(Jean,Dupont,30)

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:

E:\data\serge\JAVA\classes\interne>java test1
art=article(a100,velo,1000.0,10,5)

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

public class C : Enumeration{
    ...
    boolean hasMoreElements(){....}
    Object nextElement(){...}
}

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:

17
21
127
201

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

public class classeDérivée extends classeDeBase implements i1,i2,..,in{
...
}

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:

17
21
127
201
1047
2001

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:

E:\data\serge\JAVA\classes\anonyme>java test4
10007
2001

3.6. Pakiety

3.6.1. Tworzenie klas w pakiecie

Aby wyświetlić wiersz na ekranie, używamy instrukcji

System.out.println(...)

Jeśli przyjrzymy się definicji klasy System, zauważymy, że w rzeczywistości nazywa się ona java.lang.System:

Image

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:

import java.lang.*;

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:

package paquetage;
// definicja klasy
...

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

import java.util.*;

, 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:

Image

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:

Image

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:

Image

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:

Image

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

    24740        0.15        2072.5

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:

    0                0.65        49062

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 :