3. Класи та інтерфейси
3.1. Об’єкт на прикладі
3.1.1. Загальні відомості
Тепер на прикладі розглянемо об'єктне програмування. Об'єкт — це суть, що містить дані, які визначають його стан (їх називають атрибутами або властивостями), та функції (їх називають методами). Об'єкт створюється за зразком, який називається класом:
public class C1{
type1 p1; // властивість p1
type2 p2; // властивість p2
…
type3 m3(…){ // метод m3
…
}
type4 m4(…){ // метод m4
…
}
…
}
На основі попереднього класу C1 можна створити багато об’єктів O1, O2,… Усі вони матимуть властивості p1, p2,… та методи m3, m4,… Вони матимуть різні значення для своїх властивостей pi, отже кожен з них матиме власний стан.
Якщо O1 є об’єктом типу C1, то O1.p1 позначає властивість p1 об’єкта O1, а O1.m1 — метод m1 об’єкта O1.
Розглянемо першу модель об’єкта: клас personne.
3.1.2. Визначення класу «особа»
Визначення класу personne буде таким:
import java.io.*;
public class personne{
// атрибути
private String prenom;
private String nom;
private int age;
// метод
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
// метод
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
Тут ми маємо визначення класу, тобто типу даних. Коли ми будемо створювати змінні цього типу, ми називатимемо їх об’єктами. Отже, клас — це форма, на основі якої створюються об’єкти.
Елементами або полями класу можуть бути дані або методи (функції). Ці поля можуть мати один із трьох таких атрибутів:
privé: Приватне поле (private) доступне лише для внутрішніх методів класу
public: Публічне поле доступне для будь-якої функції, незалежно від того, чи визначена вона в межах класу
protégé: Поле захищеного доступу (protected) доступне лише для внутрішніх методів класу або похідного об’єкта (див. далі поняття успадкування).
Зазвичай дані класу оголошуються приватними, тоді як його методи — публічними. Це означає, що користувач об’єкта (програміст)
а: не матиме прямого доступу до приватних даних об’єкта
б: зможе викликати публічні методи об’єкта, зокрема ті, що надають доступ до його приватних даних.
Синтаксис оголошення об’єкта такий:
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
- Порядок оголошення атрибутів private, protected та public може бути довільним.
3.1.3. Метод initialise
Повернемося до нашого класу «person», оголошеного так:
import java.io.*;
public class personne{
// атрибути
private String prenom;
private String nom;
private int age;
// метод
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
// метод
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
Яку роль відіграє метод initialise? Оскільки прізвище, ім’я та вік є приватними даними класу «особа», інструкції
є недопустимими. Нам потрібно ініціалізувати об’єкт типу personne за допомогою публічного методу. Саме для цього призначений метод initialise. Ми напишемо:
Запис p1.initialise є допустимим, оскільки initialise має публічний доступ.
3.1.4. Оператор new
Послідовність команд
є неправильною. Команда
оголошує p1 як посилання на об’єкт типу personne. Цей об’єкт ще не існує, а отже p1 не ініціалізовано. Це так, ніби ми написали:
де за допомогою ключового слова null явно вказується, що змінна p1 ще не посилається на жоден об’єкт.
Коли потім записуємо
, ми викликаємо метод initialise об’єкта, на який посилається p1. Однак цей об’єкт ще не існує, і компілятор повідомить про помилку. Щоб p1 посилався на об’єкт, потрібно написати:
Це призведе до створення ще не ініціалізованого об’єкта типу personne: атрибути nom та prenom, які є посиланнями на об’єкти типу String, матимуть значення null, а age — значення 0. Отже, відбувається ініціалізація за замовчуванням. Тепер, коли p1 посилається на об’єкт, інструкція ініціалізації цього об’єкта
є дійсною.
3.1.5. Ключове слово this
Розглянемо код методу initialise:
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
Інструкція this.prenom=P означає, що атрибут prenom поточного об’єкта (this) отримує значення P. Ключове слово this позначає поточний об’єкт: той, у якому знаходиться метод, що виконується. Звідки ми це знаємо? Давайте подивимося, як відбувається ініціалізація об’єкта, на який посилається p1 у програмі, що викликає:
Викликається метод initialise об’єкта p1. Коли в цьому методі згадується об’єкт this, насправді мається на увазі об’єкт p1. Метод initialise також можна було б написати так:
public void initialise(String P, String N, int age){
prenom=P;
nom=N;
this.age=age;
}
Коли метод об'єкта посилається на атрибут A цього об'єкта, запис this.A є неявним. Його слід використовувати явно, коли виникає конфлікт ідентифікаторів. Це стосується такої інструкції:
this.age=age;
де age позначає атрибут поточного об’єкта, а також параметр age, отриманий методом. У такому разі необхідно усунути неоднозначність, позначивши атрибут age як this.age.
3.1.6. Тестова програма
Ось тестова програма:
public class test1{
public static void main(String arg[]){
personne p1=new personne();
p1.initialise("Jean","Dupont",30);
p1.identifie();
}
}
Клас personne визначено у вихідному файлі personne.java і скомпільовано:
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
Те саме робимо для тестової програми:
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
Може викликати подив, що програма test1.java не імпортує клас personne за допомогою інструкції:
Коли компілятор зустрічає у вихідному коді посилання на клас, який не визначено в цьому ж вихідному файлі, він шукає цей клас у різних місцях:
- у пакетах, імпортованих за допомогою команд import
- у каталозі, з якого було запущено компілятор
У нашому прикладі компілятор було запущено з каталогу, що містить файл personne.class, що пояснює, чому він знайшов визначення класу personne. Введення в цьому випадку команди import спричиняє помилку компіляції:
E:\data\serge\JAVA\BASES\OBJETS\2>javac test1.java
test1.java:1: '.' expected
import personne;
^
1 error
Щоб уникнути цієї помилки, але нагадати, що клас «person» потрібно імпортувати, надалі на початку програми слід писати:
Тепер ми можемо запустити файл test1.class:
Можна об'єднати кілька класів в одному вихідному файлі. Отже, об'єднаємо класи personne та test1 у вихідний файл test2.java. Клас test1 перейменовано на test2 з урахуванням зміни імені вихідного файлу:
// імпортовані пакети
import java.io.*;
class personne{
// атрибути
private String prenom; // ім'я моєї особи
private String nom; // її прізвище
private int age; // його вік
// метод
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}//ініціалізує
// метод
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}//ідентифікує
}//клас
public class test2{
public static void main(String arg[]){
personne p1=new personne();
p1.initialise("Jean","Dupont",30);
p1.identifie();
}
}
Слід зауважити, що клас personne більше не має атрибута public. Насправді у вихідному файлі Java лише один клас може мати атрибут public. Це той клас, який має функцію main. Крім того, вихідний файл повинен мати ім’я, що відповідає назві цієї функції. Скомпілюємо файл 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
Зверніть увагу, що для кожного класу, присутнього у вихідному файлі, було згенеровано файл .class. Тепер запустимо файл test2.class:
Надалі ми будемо використовувати обидва методи без розрізнення:
- класи, об’єднані в один вихідний файл
- один клас на один вихідний файл
3.1.7. Інший метод ініціалізує
Розглянемо знову клас personne і додамо до нього такий метод:
public void initialise(personne P){
prenom=P.prenom;
nom=P.nom;
this.age=P.age;
}
Зараз у нас є два методи з назвою initialise: це допустимо, якщо вони приймають різні параметри. У даному випадку саме так і є. Параметром тепер є посилання P на особу. Атрибути особи P при цьому присвоюються поточному об’єкту (this). Слід зауважити, що метод initialise має прямий доступ до атрибутів об’єкта P, хоча вони мають тип private. Це завжди так: методи об’єкта O1 класу C завжди мають доступ до приватних атрибутів інших об’єктів того самого класу C.
Ось тест нового класу personne:
// імпортувати особу;
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();
}
}
та його результати:
3.1.8. Конструктори класу «особа»
Конструктор — це метод, який має назву класу і викликається під час створення об’єкта. Зазвичай його використовують для ініціалізації об’єкта. Цей метод може приймати аргументи, але не повертає жодного результату. Перед його прототипом або визначенням не вказується жоден тип (навіть void).
Якщо клас має конструктор, що приймає n аргументів argi, оголошення та ініціалізація об’єкта цього класу можуть здійснюватися у такій формі:
classe objet =new classe(arg1,arg2, ... argn);
або
classe objet;
…
objet=new classe(arg1,arg2, ... argn);
Якщо клас має один або кілька конструкторів, для створення об’єкта цього класу обов’язково має бути використаний один із цих конструкторів. Якщо клас C не має жодного конструктора, він має конструктор за замовчуванням — конструктор без параметрів: public C(). Атрибути об’єкта тоді ініціалізуються значеннями за замовчуванням. Саме це відбувалося, коли в попередніх програмах ми писали:
Створимо два конструктори для нашого класу personne:
public class personne{
// атрибути
private String prenom;
private String nom;
private int age;
// конструктори
public personne(String P, String N, int age){
initialise(P,N,age);
}
public personne(personne P){
initialise(P);
}
// метод
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;
}
// метод
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
Наші два конструктори обмежуються викликом відповідних методів initialise. Нагадаємо, що коли в конструкторі зустрічається, наприклад, позначення initialise(P), компілятор перетворює його на this.initialise(P). Отже, у конструкторі викликається метод initialise для роботи з об’єктом, на який посилається this, тобто з поточним об’єктом, який саме створюється.
Ось тестова програма:
// імпорт «особа»;
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();
}
}
та отримані результати:
3.1.9. Посилання на об’єкти
Ми як і раніше використовуємо той самий клас personne. Тестова програма виглядає наступним чином:
// імпортувати «особа»;
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 посилається на той самий об’єкт, що й p1
personne p2=p1;
System.out.print("p2="); p2.identifie();
// p3 посилається на об’єкт, який буде копією об’єкта, на який посилається p1
personne p3=new personne(p1);
System.out.print("p3="); p3.identifie();
// змінюємо стан об’єкта, на який посилається p1
p1.initialise("Micheline","Benoît",67);
System.out.print("p1="); p1.identifie();
// оскільки p2=p1, стан об’єкта, на який посилається p2, мав змінитися
System.out.print("p2="); p2.identifie();
// оскільки p3 не посилається на той самий об’єкт, що й p1, об’єкт, на який посилається p3, не мав змінитися
System.out.print("p3="); p3.identifie();
}
}
Отримано такі результати:
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
Коли оголошується змінна p1 за допомогою
p1 посилається на об’єкт personne("Jean","Dupont",30), але не є самим об’єктом. У мові C це можна порівняти з покажчиком — c.a.d — адресою створеного об’єкта. Якщо потім написати:
Змінюється не сам об’єкт personne("Jean","Dupont",30), а значення посилання p1. Об’єкт person("Jean", "Dupont", 30) буде «втрачено», якщо на нього не посилається жодна інша змінна.
Коли ми пишемо:
ініціалізується покажчик p2: він «вказує» на той самий об’єкт (позначає той самий об’єкт), що й покажчик p1. Отже, якщо змінити об’єкт, на який «вказує» (або на який посилається) p1, то зміниться й об’єкт, на який посилається p2.
Коли ми пишемо:
створюється новий об’єкт — копія об’єкта, на який посилається p1. На цей новий об’єкт буде посилатися p3. Якщо змінити об’єкт, на який «вказує» (або на який посилається) p1, це жодним чином не вплине на об’єкт, на який посилається p3. Саме це показують отримані результати.
3.1.10. Тимчасові об’єкти
У виразі можна явно викликати конструктор об’єкта: об’єкт створюється, але ми не маємо до нього доступу (наприклад, щоб його змінити). Цей тимчасовий об’єкт створюється для обчислення виразу, а потім знищується. Пам’ять, яку він займав, згодом автоматично звільняється програмою, яка називається «збирач сміття» (garbage collector), завданням якої є звільнення пам’яті, зайнятої об’єктами, на які більше не посилаються дані програми.
Розглянемо такий приклад:
// імпортувати person;
public class test1{
public static void main(String arg[]){
new personne(new personne("Jean","Dupont",30)).identifie();
}
}
і змінимо конструктори класу personne так, щоб вони виводили повідомлення:
// виробники
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);
}
Отримуємо такі результати:
що демонструє послідовне створення двох тимчасових об’єктів.
3.1.11. Методи читання та запису приватних атрибутів
Ми додаємо до класу personne методи, необхідні для читання або зміни стану атрибутів об’єктів:
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);
}
// допоміжні підприємства
public String getPrenom(){
return prenom;
}
public String getNom(){
return nom;
}
public int getAge(){
return age;
}
//модифікатори
public void setPrenom(String P){
this.prenom=P;
}
public void setNom(String N){
this.nom=N;
}
public void setAge(int age){
this.age=age;
}
}
Ми тестуємо новий клас за допомогою такої програми:
// імпорт осіб;
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()+")");
}
}
і отримуємо такі результати:
3.1.12. Методи та атрибути класу
Припустимо, що ми хочемо підрахувати кількість об’єктів personne, створених у додатку. Можна самостійно вести лічильник, але є ризик пропустити тимчасові об’єкти, які створюються то тут, то там. Набагато надійніше було б включити в конструктори класу personne інструкцію, що інкрементує лічильник. Проблема полягає в тому, щоб передати посилання на цей лічильник, щоб конструктор міг його інкрементувати: для цього потрібно передати їм новий параметр. Також можна включити лічильник у визначення класу. Оскільки це атрибут самого класу, а не конкретного об’єкта цього класу, його оголошують інакше за допомогою ключового слова static:
Щоб зробити посилання на нього, пишемо personne.nbPersonnes, щоб показати, що це атрибут самого класу personne. Тут ми створили приватний атрибут, до якого не буде прямого доступу поза межами класу. Тому ми створюємо публічний метод, щоб надати доступ до атрибута класу nbPersonnes. Щоб отримати значення nbPersonnes, методу не потрібен конкретний об’єкт: адже nbPersonnes не є атрибутом конкретного об’єкта, а є атрибутом усього класу. Тому потрібен метод класу, який також оголошується як static:
який ззовні викликатиметься за синтаксисом personne.getNbPersonnes(). Ось приклад.
Клас personne виглядає наступним чином:
public class personne{
// атрибут класу
private static long nbPersonnes=0;
// атрибути об’єктів
…
// конструктори
public personne(String P, String N, int age){
initialise(P,N,age);
nbPersonnes++;
}
public personne(personne P){
initialise(P);
nbPersonnes++;
}
// метод
…
// метод класу
public static long getNbPersonnes(){
return nbPersonnes;
}
}// клас
За допомогою такої програми:
// імпорт 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
отримуємо такі результати:
3.1.13. Передача об’єкта у функцію
Ми вже зазначали, що в Java фактичні параметри функції передаються за значенням: значення фактичних параметрів копіюються у формальні параметри. Отже, функція не може змінювати фактичні параметри.
У випадку з об’єктом не слід давати себе ввести в оману систематичним неправильним вживанням терміна «об’єкт» замість «посилання на об’єкт». Об’єкт обробляється виключно через посилання (покажчик) на нього. Отже, до функції передається не сам об’єкт, а посилання на цей об’єкт. Таким чином, у формальному параметрі копіюється значення посилання, а не значення самого об’єкта: створення нового об’єкта не відбувається.
Якщо посилання на об’єкт R1 передається до функції, воно буде скопійовано у відповідний формальний параметр R2. Отже, посилання R2 та R1 вказують на один і той самий об’єкт. Якщо функція змінює об’єкт, на який вказує R2, вона, звісно, змінює й той, на який вказує R1, оскільки це один і той самий об’єкт.

Це ілюструє наступний приклад:
// імпортувати 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();
}// modifie
}// клас
Метод modifie оголошується як static, оскільки це метод класу: для його виклику не потрібно додавати префікс об’єкта. Отримано такі результати:
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
Бачимо, що створюється лише один об’єкт: об’єкт особи p1 функції main, і що цей об’єкт дійсно було змінено функцією modifie.
3.1.14. Інкапсуляція вихідних параметрів функції в об’єкт
Через передачу параметрів за значенням у Java неможливо написати функцію, яка б мала, наприклад, вихідні параметри типу int, оскільки неможливо передати посилання на тип int, який не є об’єктом. Тоді можна створити клас, що інкапсулює тип 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;
}
}
Попередній клас має конструктор, що дозволяє ініціалізувати ціле число, та два методи, що дозволяють зчитувати та змінювати значення цього числа. Ми перевіряємо цей клас за допомогою такої програми:
// імпортувати цілі числа;
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);
}
}
і отримуємо такі результати:
3.1.15. Масив осіб
Об’єкт — це дані, як і будь-які інші, і тому кілька об’єктів можна об’єднати в масив:
// імпортувати «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();
}
}
Інструкція *person[] friends = new person[3]; створює масив із 3 елементів типу *personne*. Ці 3 елементи ініціалізуються тут значеннями null та c.a.d, оскільки вони не посилаються на жодний об’єкт. Знову ж таки, через неточність формулювання, ми говоримо про масив об’єктів, хоча насправді це лише масив посилань на об’єкти. Створення масиву об’єктів, який сам є об’єктом (наявність new*), саме по собі не створює жодного об’єкта типу його елементів: це потрібно зробити згодом.
Отримуємо такі результати:
----------------
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. Спадщина, що передається власним прикладом
3.2.1. Загальні відомості
Тут ми розглянемо поняття успадкування. Мета успадкування полягає в тому, щоб «персоналізувати» існуючий клас, щоб він задовольняв наші потреби. Припустимо, що ми хочемо створити клас enseignant: викладач — це особлива людина. Він має атрибути, яких не матиме інша людина: наприклад, предмет, який він викладає. Але він також має атрибути, властиві будь-якій людині: ім’я, прізвище та вік. Отже, вчитель повністю належить до класу personne, але має додаткові атрибути. Замість того, щоб створювати клас enseignant з нуля, краще скористатися набором властивостей класу personne, який ми адаптуємо до особливого характеру вчителів. Це дозволяє зробити концепція успадкування.
Щоб показати, що клас enseignant успадковує властивості класу personne, ми напишемо:
public class enseignant extends personne
personne називається батьківським (або материнським) класом, а enseignant — похідним (або дочірнім) класом. Об’єкт enseignant має всі властивості об’єкта personne: він має ті самі атрибути та ті самі методи. Ці атрибути та методи батьківського класу не повторюються у визначенні дочірнього класу: достатньо вказати лише атрибути та методи, додані дочірнім класом:
class enseignant extends personne{
// атрибути
private int section;
// конструктор
public enseignant(String P, String N, int age,int section){
super(P,N,age);
this.section=section;
}
}
Припустимо, що клас personne визначено таким чином:
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+")";
}
// методи доступу
public String getPrenom(){
return prenom;
}
public String getNom(){
return nom;
}
public int getAge(){
return age;
}
//модифікатори
public void setPrenom(String P){
this.prenom=P;
}
public void setNom(String N){
this.nom=N;
}
public void setAge(int age){
this.age=age;
}
}
Метод identifie було дещо змінено для повернення рядка символів, що ідентифікує особу, і тепер він має назву identite. Тут клас enseignant доповнює методи та атрибути класу personne:
- атрибут section, який є номером секції, до якої належить викладач у колективі викладачів (загалом одна секція на дисципліну)
- новий конструктор, що дозволяє ініціалізувати всі атрибути викладача
3.2.2. Створення об’єкта «викладач»
Конструктор класу enseignant має такий вигляд:
// конструктор
public enseignant(String P, String N, int age,int section){
super(P,N,age);
this.section=section;
}
Інструкція super(P,N,age) є викликом конструктора батьківського класу, в даному випадку класу personne. Відомо, що цей конструктор ініціалізує поля prenom, nom та age об’єкта personne, що міститься всередині об’єкта étudiant. Це виглядає досить складно, і можна було б написати так:
// конструктор
public enseignant(String P, String N, int age,int section){
this.prenom=P;
this.nom=N
this.age=age
this.section=section;
}
Це неможливо. Клас personne оголосив свої три поля prenom, nom та age приватними (private). Прямий доступ до цих полів мають лише об’єкти того самого класу. Усі інші об’єкти, включаючи похідні об’єкти, як у цьому випадку, повинні використовувати публічні методи, щоб отримати доступ до них. Ситуація була б іншою, якби клас personne оголосив ці три поля захищеними (protected): тоді він дозволив би похідним класам мати прямий доступ до цих трьох полів. У нашому прикладі використання конструктора батьківського класу було, отже, правильним рішенням, і це є стандартним підходом: під час створення дочірнього об’єкта спочатку викликається конструктор батьківського об’єкта, а потім виконуються власні ініціалізації, що стосуються саме дочірнього об’єкта (section у нашому прикладі).
Спробуємо написати першу програму:
// імпорт особи;
// імпортувати викладача;
public class test1{
public static void main(String arg[]){
System.out.println(new enseignant("Jean","Dupont",30,27).identite());
}
}
Ця програма обмежується створенням об'єкта enseignant (new) та його ідентифікацією. Клас enseignant не має методу identité, але його батьківський клас має такий метод, який, до того ж, є публічним: завдяки успадкуванню він стає публічним методом класу enseignant.
Вихідні файли класів збираються в одному каталозі, а потім компілюються:
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
Виконується файл test1.class:
3.2.3. Перевантаження методу
У попередньому прикладі ми отримали ідентифікатор частини personne, що відповідає викладачеві, але бракує певної інформації, характерної для класу enseignant (секція). Тому доводиться написати метод, що дозволяє ідентифікувати викладача:
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+")";
}
}
Метод identite класу enseignant спирається на метод identite свого батьківського класу (super.identite) для відображення своєї частини «personne», а потім доповнює її полем section, яке є власним для класу enseignant.
Клас enseignant тепер має два методи identite:
- один успадкований від батьківського класу personne
- та власний
Якщо E є об’єктом класу enseignant, то E.identite позначає метод identite класу enseignant. Кажуть, що метод identite батьківського класу «перевантажується» методом identite дочірнього класу. Загалом, якщо O є об’єктом, а M — методом, для виконання методу O.M система шукає метод M у такому порядку:
- у класі об’єкта O
- у його батьківському класі, якщо такий є
- у батьківському класі його батьківського класу, якщо такий існує
- і т. д.
Отже, успадкування дозволяє переопределяти в дочірньому класі методи з тими самими іменами, що й у батьківському класі. Саме це дає змогу адаптувати дочірній клас до власних потреб. У поєднанні з поліморфізмом, про який ми поговоримо трохи пізніше, переопределення методів є головною перевагою успадкування.
Розглянемо той самий приклад, що й раніше:
// імпортувати «особа»;
// імпортувати «викладач»;
public class test1{
public static void main(String arg[]){
System.out.println(new enseignant("Jean","Dupont",30,27).identite());
}
}
Цього разу отримано такі результати:
3.2.4. Поліморфізм
Розглянемо лінію класів: C0 C1 C2 … Cn
де Ci Cj вказує, що клас Cj є похідним від класу Ci. Звідси випливає, що клас Cj має всі характеристики класу Ci, а також додаткові. Нехай Oi — об’єкти типу Ci. Допустимо записати:
Адже завдяки успадкуванню клас Cj має всі характеристики класу Ci, а також додаткові. Отже, об’єкт Oj типу Cj містить у собі об’єкт типу Ci. Операція
призводить до того, що Oi є посиланням на об’єкт типу Ci, що міститься в об’єкті Oj.
Той факт, щозмінна Oi класу Ci може фактично посилатися не лише на об’єкт класу Ci, а й на будь-який об’єкт, похідний від класу Ci, називається поліморфізмом: здатність змінної посилатися на об’єкти різних типів.
Розглянемо приклад і проаналізуємо таку функцію, незалежну від будь-якого класу:
Клас Object є «батьківським» для всіх класів Java. Отже, коли ми пишемо:
ми неявним чином пишемо:
Отже, кожен об’єкт Java містить у собі частину типу Object. Таким чином, можна написати:
Формальний параметр типу Object функції affiche отримає значення типу enseignant. Оскільки enseignant походить від Object, це допустимо.
3.2.5. Перевантаження та поліморфізм
Доповнимо нашу функцію affiche:
Метод obj.toString() повертає рядок, що ідентифікує об’єкт obj у форматі nom_de_la_classe@adresse_de_l'об’єкт. Що відбувається у випадку нашого попереднього прикладу:
Система повинна виконати інструкцію System.out.println(e.toString()), де e є об’єктом-викладачем. Він шукає метод toString в ієрархії класів, що веде до класу enseignant, починаючи з останнього:
- у класі enseignant він не знаходить методу toString()
- у батьківському класі personne він не знаходить методу toString()
- у батьківському класі Object він знаходить метод toString() і виконує його
Це ілюструє наступна програма:
// імпорт особи;
// імпорт викладача;
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());
}
}
Отримано такі результати:
Тобто об’єкт nom_de_la_classe@adresse_de_l. Оскільки це не дуже зрозуміло, виникає спокуса визначити метод toString для класів personne та etudiant, який би переопределяв метод toString батьківського класу Object. Замість того, щоб писати методи, які були б схожими на вже існуючі методи identite у класах personne та enseignant, просто перейменуємо ці методи identite на 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+")";
}
}
За допомогою тієї ж програми тестування, що й раніше, отримано такі результати:
3.3. Внутрішні класи
Клас може містити визначення іншого класу. Розглянемо такий приклад:
// імпортовані класи
import java.io.*;
public class test1{
// внутрішній клас
private class article{
// визначаємо структуру
private String code;
private String nom;
private double prix;
private int stockActuel;
private int stockMinimum;
// конструктор
public article(String code, String nom, double prix, int stockActuel, int stockMinimum){
// ініціалізація атрибутів
this.code=code;
this.nom=nom;
this.prix=prix;
this.stockActuel=stockActuel;
this.stockMinimum=stockMinimum;
}//конструктор
//toString
public String toString(){
return "article("+code+","+nom+","+prix+","+stockActuel+","+stockMinimum+")";
}//toString
}//клас товару
// локальні дані
private article art=null;
// виробник
public test1(String code, String nom, double prix, int stockActuel, int stockMinimum){
// визначення атрибута
art=new article(code, nom, prix, stockActuel,stockMinimum);
}//test1
// метод доступу
public article getArticle(){
return art;
}//getArticle
public static void main(String arg[]){
// створення екземпляра test1
test1 t1=new test1("a100","velo",1000,10,5);
// виведення на екран test1.art
System.out.println("art="+t1.getArticle());
}//головна
}// кінець класу
Клас test1 містить визначення іншого класу — класу article. Кажуть, що article є внутрішнім класом класу test1. Це може бути корисно, коли внутрішній клас має значення лише в тому класі, який його містить. Під час компіляції наведеного вище вихідного коду test1.java отримуємо два файли .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
Файл test1$article.class було згенеровано для класу article, що є внутрішнім для класу test1. Якщо виконати наведену вище програму, отримаємо такі результати:
3.4. Інтерфейси
Інтерфейс — це набір прототипів методів або властивостей, що утворюють контракт. Клас, який вирішує реалізувати інтерфейс, зобов’язується надати реалізацію всіх методів, визначених в інтерфейсі. Перевірку цієї реалізації здійснює компілятор.
Ось, наприклад, визначення інтерфейсу java.util.Enumeration:
Зведена інформація про методи | ||
boolean | hasMoreElements() Перевіряє, чи містить цей перелік більше елементів. | |
Об'єкт | nextElement() Повертає наступний елемент цього переліку, якщо цей об’єкт переліку має принаймні ще один елемент для надання. | |
Будь-який клас, що реалізує цей інтерфейс, буде оголошений як
Методи hasMoreElements() та nextElement() повинні бути визначені в класі C.
Розглянемо наступний код, що визначає клас élève, який задає ім’я учня та його оцінку з предмета:
// клас «учень»
public class élève{
// публічні атрибути
public String nom;
public double note;
// конструктор
public élève(String NOM, double NOTE){
nom=NOM;
note=NOTE;
}//конструктор
}//учень
Ми визначаємо клас notes, що об’єднує оцінки всіх учнів з одного предмета:
// імпортовані класи
// імпорт об’єкта «учень»
// клас «оцінки»
public class notes{
// атрибути
protected String matière;
protected élève[] élèves;
// конструктор
public notes (String MATIERE, élève[] ELEVES){
// збереження даних про учнів та предмети
matière=MATIERE;
élèves=ELEVES;
}//оцінки
// toString
public String toString(){
String valeur="matière="+matière +", notes=(";
int i;
// об'єднання всіх оцінок
for (i=0;i<élèves.length-1;i++){
valeur+="["+élèves[i].nom+","+élèves[i].note+"],";
};
//остання оцінка
if(élèves.length!=0){ valeur+="["+élèves[i].nom+","+élèves[i].note+"]";}
valeur+=")";
// кінець
return valeur;
}//toString
}//клас
Атрибути matière та élèves оголошуються як protected, щоб бути доступними з похідного класу. Ми вирішили створити похідний клас notes від класу notesStats, який матиме два додаткові атрибути — середнє значення та стандартне відхилення оцінок:
public class notesStats extends notes implements Istats {
// атрибути
private double _moyenne;
private double _écartType;
Клас notesStats походить від класу notes і реалізує наступний інтерфейс Istats:
// інтерфейс
public interface Istats{
double moyenne();
double écartType();
}//
Це означає, що клас notesStats повинен мати два методи з іменами moyenne та écartType із сигнатурою, вказаною в інтерфейсі Istats. Клас notesStats має такий вигляд:
// імпортовані класи
// імпорт нотаток;
// імпорт Istats;
// імпорт «учень»;
public class notesStats extends notes implements Istats {
// атрибути
private double _moyenne;
private double _écartType;
// конструктор
public notesStats (String MATIERE, élève[] ELEVES){
// створення батьківського класу
super(MATIERE,ELEVES);
// обчислення середнього балу
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;
// стандартне відхилення
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;
}//конструктор
// ToString
public String toString(){
return super.toString()+",moyenne="+_moyenne+",écart-type="+_écartType;
}//ToString
// методи інтерфейсу Istats
public double moyenne(){
// обчислює середнє значення оцінок
return _moyenne;
}//середнє
public double écartType(){
// повертає стандартне відхилення
return _écartType;
}//écartType
}//клас
Середнє значення _moyenne та стандартне відхилення _ecartType обчислюються під час створення об’єкта. Тому методи moyenne та écartType повинні лише повертати значення атрибутів _moyenne та _ecartType. Обидва методи повертають -1, якщо масив учнів порожній.
Наступний тестовий клас:
// імпортовані класи
// імпорт учнів;
// імпорт Istats;
// імпорт оцінок;
// імпорт notesStats;
// тестовий клас
public class test{
public static void main(String[] args){
// деякі учні та оцінки
élève[] ELEVES=new élève[] { new élève("paul",14),new élève("nicole",16), new élève("jacques",18)};
// які записуються в об’єкт «оцінки»
notes anglais=new notes("anglais",ELEVES);
// і які відображаються
System.out.println(""+anglais);
// те саме з середнім значенням та стандартним відхиленням
anglais=new notesStats("anglais",ELEVES);
System.out.println(""+anglais);
}//головна
}//клас
дає такі результати:
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
Кожен із класів у цьому прикладі міститься в окремому файлі вихідного коду:
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
Клас notesStats цілком міг би реалізувати методи moyenne та écartType для себе, не вказуючи, що він реалізує інтерфейс Istats. То в чому ж полягає користь інтерфейсів? Справа в тому, що функція може приймати як параметр дані типу інтерфейсу I. Тоді будь-який об’єкт класу C, що реалізує інтерфейс I, може бути параметром цієї функції. Розглянемо такий інтерфейс:
// інтерфейс Iexample
public interface Iexemple{
int ajouter(int i,int j);
int soustraire(int i,int j);
}//інтерфейс
Інтерфейс Iexemple визначає два методи: ajouter та soustraire. Наступні класи classe1 та classe2 реалізують цей інтерфейс.
// імпортовані класи
// імпорт Iexample;
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;
}
}//клас
// імпортовані класи
// імпорт 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;
}
}//клас
Для спрощення прикладу класи лише реалізують інтерфейс Iexemple. Тепер розглянемо наступний приклад:
// імпортовані класи
// імпорт класу1;
// імпорт класу2;
// тестовий клас
public class test{
// статична функція
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//обчислити
// функція main
public static void main(String[] arg){
// створення двох об’єктів класу1 та класу2
classe1 c1=new classe1();
classe2 c2=new classe2();
// виклики статичної функції «calculer»
calculer(4,3,c1);
calculer(14,13,c2);
}//main
}//клас test
Статична функція calculer приймає як параметр елемент типу Iexemple. Отже, вона може приймати для цього параметра як об’єкт типу classe1, так і об’єкт типу classe2. Саме це робиться у функції main з такими результатами:
Отже, бачимо, що тут маємо властивість, близьку до поліморфізму, який ми розглядали для класів. Тому, якщо набір класів Ci, що не пов’язані між собою спадковістю (тобто не можна використовувати поліморфізм спадковості), має набір методів з однаковою сигнатурою, може бути доцільним об’єднати ці методи в інтерфейс I, який успадковуватимуть усі відповідні класи. Інстанції цих класів Ci можна тоді використовувати як параметри функцій, що приймають параметр типу I, c.a.d, — функцій, які використовують лише методи об’єктів Ci, визначені в інтерфейсі I, а не конкретні атрибути та методи окремих класів Ci.
У попередньому прикладі кожен клас або інтерфейс містився в окремому вихідному файлі:
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
Нарешті, зауважимо, що успадкування інтерфейсів може бути множинним, c.a.d. Це можна записати так:
де ij — це інтерфейси.
3.5. Анонімні класи
У попередньому прикладі класи classe1 та classe2 можна було б не визначати явно. Розглянемо наступну програму, яка виконує практично те саме, що й попередня, але без явного визначення класів classe1 та classe2:
// імпортовані класи
// імпорт Iexemple;
// тестовий клас
public class test2{
// внутрішній клас
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;
}
};//визначення класу3
// статична функція
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//обчислити
// функція main
public static void main(String[] arg){
// створення двох об’єктів, що реалізують інтерфейс 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;
}
};//визначення 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;
}
};//визначення i2
// ще один об’єкт Iexemple
Iexemple i3=new classe3();
// виклики статичної функції «calculer»
calculer(4,3,i1);
calculer(14,13,i2);
calculer(24,23,i3);
}//main
}//клас test
Особливість полягає в коді:
// створення двох об’єктів, що реалізують інтерфейс 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;
}
};//визначення i1
Створюється об’єкт i1, єдиною роллю якого є реалізація інтерфейсу Iexemple. Цей об’єкт має тип Iexemple. Отже, можна створювати об’єкти типу інтерфейсу. Дуже багато методів класів Java повертають об’єкти типу інтерфейсу c.a.d — об’єкти, єдиною роллю яких є реалізація методів інтерфейсу. Щоб створити об’єкт i1, може виникнути спокуса написати:
Iexemple i1=new Iexemple()
Однак інтерфейс не можна інстанціювати. Інстанціювати можна лише клас, що реалізує цей інтерфейс. У цьому випадку ми визначаємо такий клас «на льоту» безпосередньо в тілі визначення об’єкта i1:
Iexemple i1=new Iexemple(){
public int ajouter(int a, int b){
// визначення функції «додати»
}
public int soustraire(int a, int b){
// визначення функції «відняти»
}
};//визначення i1
Значення такої інструкції аналогічне послідовності:
public class test2{
................
// внутрішній клас
private static class classe1 implements Iexemple{
public int ajouter(int a, int b){
// визначення додавання
}
public int soustraire(int a, int b){
// визначення віднімання
}
};//визначення класу1
.................
public static void main(String[] arg){
...........
Iexemple i1=new classe1();
}//main
}//клас
У наведеному вище прикладі створюється екземпляр класу, а не інтерфейсу. Клас, визначений «на льоту», називається анонімним класом. Цей метод часто використовується для створення екземплярів об’єктів, єдиною роллю яких є реалізація інтерфейсу.
Виконання попередньої програми дає такі результати:
У попередньому прикладі для реалізації інтерфейсу використовувалися анонімні класи. Їх також можна використовувати для створення похідних класів, що не мають конструкторів з параметрами. Розглянемо такий приклад:
// імпортовані класи
// імпорт 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;
}
};//визначення класу3
public class test4{
// статична функція
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//обчислити
// метод main
public static void main(String args[]){
// визначення анонімного класу, що успадковує клас3
// для перевизначення функції «віднімати»
classe3 i1=new classe3(){
public int ajouter(int a, int b){
return a+b+10000;
}//віднімати
};//i1
// виклики статичної функції «calculer»
calculer(4,3,i1);
}//main
}//клас
Тут ми бачимо клас classe3, що реалізує інтерфейс Iexemple. У функції main ми визначаємо змінну i1, тип якої — клас, похідний від classe3. Цей похідний клас визначається «на льоту» в анонімному класі та перевизначає метод ajouter класу classe3. Синтаксис ідентичний синтаксису анонімного класу, що реалізує інтерфейс. Тільки тут компілятор виявляє, що classe3 — це не інтерфейс, а клас. Тому для нього це є похідним класом. Усі методи, які він знайде в тілі анонімного класу, замінять методи з такими самими іменами у базовому класі.
Виконання попередньої програми дає такі результати:
3.6. Пакети
3.6.1. Створення класів у пакеті
Щоб вивести рядок на екран, ми використовуємо оператор
Якщо ми подивимося на визначення класу System, то побачимо, що насправді він називається java.lang.System:

Перевіримо це на прикладі:
public class test1{
public static void main(String[] args){
java.lang.System.out.println("Coucou");
}//main
}//клас
Скомпілюємо та запустимо цю програму:
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
Чому ж ми можемо писати
System.out.println("Coucou");
замість
java.lang.System.out.println("Coucou");
Оскільки для кожної програми Java неявним чином відбувається систематичне імпортування «пакета» java.lang. Таким чином, все відбувається так, ніби на початку кожної програми є інструкція:
Що означає ця інструкція? Вона надає доступ до всіх класів пакета java.lang. Компілятор знайде там файл System.class, що визначає клас System. Поки що невідомо, де компілятор знайде пакет java.lang і як саме виглядає пакет. Ми повернемося до цього пізніше. Щоб створити клас у пакеті, потрібно написати:
Для прикладу створимо в пакеті наш клас personne, який ми розглядали раніше. Як ім’я пакета оберемо istia.st. Клас personne стає таким:
// назва пакета, в якому буде створено клас «особа»
package istia.st;
// клас «особа»
public class personne{
// прізвище, ім’я, вік
private String prenom;
private String nom;
private int age;
// конструктор 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+")";
}
}//клас
Цей клас компілюється, а потім розміщується в каталозі istia\st поточного каталогу. Чому саме istia\st? Тому що пакет називається 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
Тепер використаємо клас personne у першому тестовому класі:
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);
}//рука
}//тестовий клас
Слід зауважити, що клас personne тепер має префікс назви свого пакета istia.st. Де компілятор знайде клас istia.st.personne? Компілятор шукає необхідні йому класи у заздалегідь визначеному списку каталогів та у деревоподібній структурі, що починається з поточного каталогу. У цьому випадку він шукатиме клас istia.st.personne у файлі istia\st\personne.class. Саме тому ми помістили файл personne.class у каталог istia\st. Скомпілюємо, а потім запустимо тестову програму:
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)
Щоб уникнути написання
istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);
клас istia.st.personne можна імпортувати за допомогою клаузули import:
import istia.st.personne;
Тоді ми можемо написати
personne p1=new personne("Jean","Dupont",20);
, і компілятор перетворить це на
istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);
Тестова програма виглядає наступним чином:
// імпортовані простори імен
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);
}//головний
}//клас test2
Скомпілюємо та запустимо цю нову програму:
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)
Ми розмістили пакет istia.st у поточному каталозі. Це не обов'язково. Давайте розмістимо його у папці з назвою mesClasses, яка також знаходиться у поточному каталозі. Нагадаємо, що класи пакета istia.st розміщені у папці istia\st. Структура поточного каталогу виглядає наступним чином:
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
Тепер знову скомпілюємо програму 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;
Компілятор більше не знаходить пакет istia.st, оскільки ми його перемістили. Зауважимо, що він шукає його через інструкцію import. За замовчуванням він шукає його, починаючи з поточного каталогу, у папці з назвою istia\st, яка більше не існує. Розглянемо параметри компілятора:
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
Тут нам може стати в нагоді опція -classpath. Вона дозволяє вказати компілятору, де шукати його класи та пакети. Спробуємо. Зкомпілюємо, вказавши компілятору, що пакет istia.st тепер знаходиться у папці 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
Цього разу компіляція проходить без проблем. Запустимо програму 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)
Тепер уже віртуальна машина Java не може знайти клас istia/st/personne. Вона шукає його в поточному каталозі, хоча він тепер знаходиться в каталозі mesClasses. Давайте подивимося на параметри віртуальної машини 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
Бачимо, що JVM також має опцію classpath, як і компілятор. Скористаємося нею, щоб вказати, де знаходиться пакет istia.st:
E:\data\serge\JAVA\classes\paquetages\personne>java.bat -classpath mesClasses test2
Exception in thread "main" java.lang.NoClassDefFoundError: test2
Ми не надто просунулися. Тепер не вдається знайти сам клас test2. Причина така: за відсутності ключового слова classpath поточний каталог систематично обшукується під час пошуку класів, але не обшукується, коли це ключове слово присутнє. Як наслідок, клас test2.class, який знаходиться в поточному каталозі, не знайдено. Рішення? Додати поточний каталог до classpath. Поточний каталог позначається символом .
E:\data\serge\JAVA\classes\paquetages\personne>java -classpath mesClasses;. test2
p1=personne(Jean,Dupont,20)
Чому такі складнощі? Мета пакетів — уникнути конфліктів імен між класами. Розглянемо дві компанії E1 та E2, які розповсюджують упаковані класи відповідно у пакетах com.e1 та com.e2. Нехай клієнт C купує ці два набори класів, у яких обидві компанії визначили клас personne. Клієнт C буде посилатися на клас personne компаніїкомпанії E1 як com.e1.personne, а клас компанії E2 — як com.e2.personne, тим самим уникнувши конфлікту імен.
3.6.2. Пошук пакетів
Коли ми пишемо в програмі
щоб отримати доступ до всіх класів пакета java.util, де його можна знайти? Ми вже зазначали, що пакети за замовчуванням шукаються в поточному каталозі або у списку каталогів, вказаних в опції classpath компілятора, або в JVM, якщо така опція присутня. Вони також шукаються в каталогах lib у каталозі встановлення JDK. Розглянемо цей каталог:

У цьому прикладі будуть проскановані дерева jdk14\lib та jdk14\jre\lib з метою пошуку або файлів .class, або файлів .jar чи .zip, які є архівами класів. Наприклад, здійснимо пошук файлів .jar, що знаходяться в попередньому каталозі jdk14:

Їх там кілька десятків. Файл .jar можна відкрити за допомогою утиліти winzip. Відкриємо вищезазначений файл rt.jar (rt=RunTime). У ньому міститься кілька сотень файлів .class, зокрема ті, що належать до пакета java.util:

Простим способом управління пакетами є їх переміщення до каталогу <jdk>\jre\lib, де <jdk> — це каталог інсталяції JDK. Зазвичай пакет містить кілька класів, і зручно об’єднати їх в один файл .jar (JAR = файл Java ARchive). Виконуваний файл jar.exe знаходиться у папці <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
Довідку щодо використання програми jar можна отримати, запустивши її без параметрів:
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/ .
Повернемося до класу personne.class, створеного раніше в пакеті 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
Створимо файл istia.st.jar, що містить усі класи пакета istia.st, тобто всі класи з наведеної вище структури 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
Давайте розглянемо за допомогою winzip вміст файлу istia.st.jar:

Помістимо файл istia.st.jar у каталог <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
Тепер скомпілюємо програму test2.java, а потім запустимо її:
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)
Можна помітити, що нам довелося лише вказати ім’я архіву, який потрібно обшукати, без необхідності явно вказувати, де він знаходиться. Усі каталоги в дереві <jdk>\jre\lib обшукуються для пошуку запитуваного файлу .jar.
3.7. Приклад IMPÔTS
Повернемося до розрахунку податку, який ми вже розглядали в попередньому розділі, і обробимо його за допомогою класу. Нагадаємо задачу:
Розглянемо спрощений випадок, коли платник податків має задекларувати лише свою заробітну плату:
- розраховуємо кількість часток працівника nbParts = nbEnfants/2 + 1, якщо він не одружений, nbEnfants/2+2, якщо він одружений, де nbEnfants — кількість його дітей.
- якщо у нього щонайменше троє дітей, він отримує на пів частки більше
- розраховується його оподатковуваний дохід R = 0,72*S, де S — його річна заробітна плата
- розраховується його сімейний коефіцієнт QF = R / nbParts
- розраховується його податок I. Розглянемо таку таблицю:
12620,0 | 0 | 0 |
13190 | 0,05 | 631 |
15640 | 0,1 | 1290,5 |
24740 | 0,15 | 2072,5 |
31810 | 0,2 | 3309,5 |
39970 | 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 |
172040 | 0,55 | 30710 |
195 000 | 0,60 | 39312 |
0 | 0,65 | 49062 |
Кожен рядок має 3 поля. Щоб обчислити податок I, шукаємо перший рядок, у якому QF <= поле1. Наприклад, якщо QF = 23000, то знайдемо рядок
Тоді податок I дорівнює 0,15*R - 2072,5*nbParts. Якщо QF є таким, що співвідношення QF<=поле1 ніколи не виконується, то використовуються коефіцієнти останнього рядка. У даному випадку:
що дає податок I=0,65*R - 49062*nbParts.
Клас impots буде визначено наступним чином:
// створення класу impots
public class impots{
// дані, необхідні для розрахунку податку
// походять із зовнішнього джерела
private double[] limites, coeffR, coeffN;
// виробник
public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
// перевіряємо, чи всі 3 масиви мають однаковий розмір
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+")");
// все гаразд
this.limites=LIMITES;
this.coeffR=COEFFR;
this.coeffN=COEFFN;
}//виробник
// розрахунок податку
public long calculer(boolean marié, int nbEnfants, int salaire){
// розрахунок кількості часток
double nbParts;
if (marié) nbParts=(double)nbEnfants/2+2;
else nbParts=(double)nbEnfants/2+1;
if (nbEnfants>=3) nbParts+=0.5;
// розрахунок оподатковуваного доходу та сімейного коефіцієнта
double revenu=0.72*salaire;
double QF=revenu/nbParts;
// розрахунок податку
limites[limites.length-1]=QF+1;
int i=0;
while(QF>limites[i]) i++;
// повернення результату
return (long)(revenu*coeffR[i]-nbParts*coeffN[i]);
}//розрахувати
}//клас
Створюється об'єкт impots з даними, що дозволяють розрахувати податок платника податків. Це стабільна частина об'єкта. Після створення цього об'єкта можна багаторазово викликати його метод calculer, який розраховує податок платника податків на основі його сімейного стану (одружений чи ні), кількості дітей та річного заробітку.
Тестова програма може виглядати так:
//імпортовані класи
// імпорт податків;
import java.io.*;
public class test
{
public static void main(String[] arg) throws IOException
{
// інтерактивна програма для розрахунку податку
// користувач вводить три значення з клавіатури: одружений nbEnfants зарплата
// після чого програма виводить суму податку до сплати
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";
// таблиці даних, необхідні для розрахунку податку
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};
// створення потоку читання
BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
// створення об’єкта податку
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
// нескінченний цикл
while(true){
// запитуються параметри розрахунку податку
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();
// що робити?
if(paramètres==null || paramètres.equals("")) break;
// перевірка кількості аргументів у введеному рядку
String[] args=paramètres.split("\\s+");
int nbParamètres=args.length;
if (nbParamètres!=3){
System.err.println(syntaxe);
continue;
}//if
// перевірка правильності параметрів
// одружений
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
// зарплата
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
// параметри правильні — розраховуємо податок
System.out.println("impôt="+objImpôt.calculer(marié.equals("o"),nbEnfants,salaire)+" F");
// наступний платник податків
}//while
}//головна
}//клас
Ось приклад виконання попередньої програми:
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 :