Оголошення enums (перерахувань) (p.60 - 65)
Починаючи із Java 5, припустимо обмежити змінну тільки одним з декількох
наперед заданих значень – іншими словами, одним значенням з переліченого списку
(окремі елементи переліченого списку називають еnums).
Використання еnum може помогти видалити помилки в коді.
Для прикладу, в програмі про кавовий магазин можна звузити вибір розмірів до BIG (BЕЛИКИЙ), HUGE (ВЕЛИЧЕЗНИЙ) та OVERWHELMING (НАДВЕЛИКИЙ). Тоді вибір LARGE і GRANDE спричинить
помилку. Спасіння в еnums.
Наступне просте оголошення гарантує, що компілятор зупинить програміста,
якщо він обере щось, крім BIG, HUGE чи OVERWHELMING:
enum CoffeeSize { BIG, HUGE,
OVERWHELMING };
Відтепер, єдиним шляхом задати значення
CoffeeSize є вираз:
CoffeeSize cs = CoffeeSize.BIG;
Не вимагається, щоб константи еnums
йменувались великими буквами, але Sun Code Convention рекомендує це.
Основними компонентами еnum є його
константи (тобто, BIG, HUGE та OVERWHELMING), хоча еnums можуть оголошувати окремий клас
або члена класу, проте вони не можуть оголошуватись всередині методу!
Оголошення еnum поза класом:
enum CoffeeSize { BIG, HUGE, OVERWHELMING
}//це не може
//бути private або
protected
class Coffee {
CoffeeSize size;
}
public class CoffeeTest1 {
public static void main(String[] args) {
Coffee drink = new Coffee();
drink.size = CoffeeSize.BIG; //enum поза класом
}
}
Вищенаведений код може бути частиною єдиного файлу. (Файл
повинен мати ім’я CoffeeTest1.java, оскільки це ім’я public класу
в файлі). Ключовий момент для запам’ятовування – це те, що еnum не
закритий в класі, тобто оголошується тільки або public, або по замовчуванню, подібно класам
верхнього рівня, а не внутрішнім класам. Ось приклад оголошення еnum всередині класу:
class Coffee2 {
enum CoffeeSize {BIG, HUGE, OVERWHELMING }
CoffeeSize size;
}
public class CoffeeTest2 {
public static void main(String[] args) {
Coffee2 drink = new Coffee2();
drink.size = Coffee2.CoffeeSize.BIG; //необхідне
//і’мя
зовнішнього класу
}
}
Ключовим моментом наведених прикладів є те, що enum можуть оголошуватись або окремим
класом, або міститись в іншому класі, тобто синтаксис доступу до членів enum залежить від місця оголошення enum. Наступний код є неправильним:
public class CoffeeTest1 {
public static void main(String[] args) {
enum CoffeeSize {BIG,HUGE,OVERWHELMING}//НЕПРАВИЛЬНО!
//Не
можна оголошувати enums всередині методу
Coffee drink = new Coffee();
drink.size = CoffeeSize.BIG;
}
}
Розробники Java зробили можливим вставляти символ крапки з комою в кінці оголошення enum
(якщо далі ніяких інших оголошень немає)
public class CoffeeTest1 {
enum CoffeeSize {BIG,HUGE,OVERWHELMING}; //крапка
із
//комою може бути, але не
обов’язково
public static void main(String[] args) {
Coffee drink = new Coffee();
drink.size = CoffeeSize.BIG;
}
}
То що ж створюється, коли робиться enum? Найважливіше, що слід
запам’ятати – enums не є елементами String чи int! Кожен перелічений тип Coffeesize є фактично екземпляром класу Coffeesize.
Іншими словами, BIG – це тип Coffeesize. Якщо представити enum різновидом класу, утвориться (не
обов’язково саме так) приблизно наступне:
// концептуальний приклад
представлення enum класом
class CoffeeSize {
public static final CoffeeSize BIG =
new
CoffeeSize("BIG", 0);
public static final CoffeeSize HUGE =
new
CoffeeSize("HUGE", 1);
public static final CoffeeSize OVERWHELMING
=
new
CoffeeSize("OVERWHELMING", 2);
public CoffeeSize(String enumName, int
index) {
// розміщення необхідного коду
}
public
static void main(String[] args) {
System.out.println(CoffeeSize.BIG);
}
}
Відмітимо, що перелічені значення BIG, HUGE,
OVERWHELMING є
екземплярами типу CoffeeSize. Вони представляються як static і final, що фактично означає константу.
Також відмітимо, що кожне значення enum знає свою позицію або індекс,
іншими словами, порядок оголошення enum має значення. Тобто, можна
представляти, що enum існують в масиві значень CoffeeSize, і, як буде показано в наступній главі, можна
ітерувати значення enum викликом методу values() будь-якого типу enum.
Оголошення
конструкторів, методів і змінних в enum
Оскільки enum – це дійсно специфічний вид класу, можна зробити більше,
ніж просто перелічити значення констант. Можна додавати конструктори, змінні екземпляру,
методи і щось справді дивне, що називається тіло
специфічного класу констант. Щоб зрозуміти, навіщо хотіти чогось більшого
від enum, представимо,
що необхідно знати справжній розмір (в унціях) кожної з трьох констант CoffeeSize. Наприклад, необхідно знати, що BIG становить 8 унцій, HUGE - 10 унцій і OVERWHELMING - 16 унцій.
Таблиця перегляду матиме бідний дизайн та складність у
використанні. Найпростіший шлях – оброблення значень enum (BIG, HUGE і OVERWHELMING) як об’єктів, що можуть мати
свої власні екземпляри змінних. Тоді можна задавати ці значення при
ініціалізації enum передаванням їх до конструктору enum:
enum CoffeeSize { //8,10,16 передаються в
конструктор
BIG(8), HUGE(10), OVERWHELMING(16);
CoffeeSize(int ounces) { // конструктор
this.ounces = ounces;
}
private int ounces; // змінна екземпляру
public int getOunces() {
return ounces;
}
}
class Coffee {
CoffeeSize size; //кожний
екземпляр Coffee має enum
public static void main(String[] args) {
Coffee
drink1 = new Coffee();
drink1.size
= CoffeeSize.BIG;
Coffee
drink2 = new Coffee();
drink2.size
= CoffeeSize.OVERWHELMING;
System.out.println(drink1.size.getOunces());//друк
8
for(CoffeeSize
cs: CoffeeSize.values())
System.out.println(cs + " " +
cs.getOunces());
}
}
Виведення:
8
BIG 8
HUGE 10
OVERWHELMING 16
Зауважимо, що кожен enum має static метод value(), що повертає масив значень enum
в порядку оголошення.
Ключові моменти щодо конструкторів enum:
1. НІКОЛИ не можнае викликати конструктор enum безпосередньо. Конструктор enum викликається автоматично з
аргументами, які вказуються після значення константи. Наприклад, BIG(8) викликає конструктор CoffeeSize, що приймає int, та передає
літеру 8 конструктору (поза кадром, звичайно, можна уявити що BIG теж передається конструктору,
але ці деталі не є обов’язковими).
2. Можна оголосити більше одного аргументу для
конструктора, можна перевантажувати конструктори enum, як і конструктори звичайних
класів. Для ініціалізації типу Coffee з кількістю унцій і, скажімо, типом кришки, необхідно передати
два аргументи конструктору, наприклад, BIG(8, "A"), що означає, що є конструктор в CoffeeSizе, який приймає разом із int
також і String.
І, нарешті, в enum можна оголосили щось дійсно
дивне, що виглядає як анонімний внутрішній клас. Це відоме як тіло специфічного
класу констант, і використовується, коли
потрібна окрема константа для перевизначення методу, оголошеного в enum.
Припустимо, що у enum потрібні 2 методи – один для унцій і один для коду
кришки (String). Припустимо також, що більшість CoffeeSizе
використовують однаковий код кришки – В, а OVERWHELMING використовує А. Можна визначити метод getLidCode() в enum CoffeeSizе, що повертає B, але потім
буде потрібно перевизначити цей метод для OVERWHELMING.
Якщо складний для використання код if/then
в методі getLidCode() є небажаним, тоді найкращим вирішенням є застосування
константи OVERWHELMING для перевизначення методу getlidcode().
Виглядає дивно, але потрібно розуміти основні правила
оголошення:
enum CoffeeSize {
BIG(8),
HUGE(10),
OVERWHELMING(16) { //початок
блоку оголошення
//"тіла" для
цієї константи
public
String getLidCode() { //перевизначення
//методу,
оголошеного всередині CoffeeSize
return
"A";
}
}; //крапка із
комою є НЕОБХІДНОЮ, якщо далі є код
CoffeeSize(int
ounces) {
this.ounces
= ounces;
}
private int
ounces;
public int
getOunces() {
return
ounces;
}
public String
getLidCode() { //метод, що
для
//константи OVERWHELMING є
перевизначеним
return
"B"; //значення, бажане за
замовчуванням
//для констант CoffeeSize
}
}
CEРТИФІКАЦІЙНE ЗАВДАННЯ
Дати , числа, та валюта (екзаменаційне питання 3.4) (р.473-487)
3.4 Використати стандарт J2SE
API в пакеті java.text package для правильного форматування або розкладання
дат, чисел та валютних величин для специфічного місцерозташування; а також, згідно із заданим сценарієм,
визначити придатні методи для використання для випадків місцерозташування за
замовчуванням або специфічного місцерозташу-вання.Опишіть призначення та
використання класу java.util.Locale.
Java API пропонує широкий (можливо, занадто широкий) набір класів для
роботи з датами, числами та валютами. На екзамені тестуються знання базових
класів і методів, що використовуються в
роботі з датами, тощо. Даний розділ надає знання зі створення нових об’єктів Date і DateFormat, взаємного перетворення між String і Date,
виконування функцій Calendaring,
друкування правильно відформатованих валютних величин, а також виконання цього
всього для будь-якого місцерозташування на земній кулі. Значною мірою включення
цих питань до іспитових завдань пов’язане із наміром протестувати вміння
реалізувати основи інтернаціоналізації
(часто скорочують до "i18n").
Робота з датами, числами і валютами
При роботі з датами скрізь для країн усього світу необхідно досконало
володіти, щонайменше, чотирьма класами пакетів java.text і java.util.
На екзамені можуть зустрітись питання, де використовуються класи, що не є
згадуються в екзаменаційних завданнях Sun. Необхідно розібратись із 4 класами,
орієнтованими на роботу із датами:
·
java.util.Date. Більшість з методів цього класу були вилучені, але можна
використовувати його для зв’язку між класами Calendar і DateFormat. Екземпляр Date представляє
змінювані значення дати і часу до мілісекунд.
·
java.util.Calendar. Цей клас містить забезпечує велику кулькість методів
для перетворення та маніпуляції датами і часом. Наприклад, методи класу Calendar застосовуються при необхідності додавання
місяця до заданої дати або визначення дня тижня 1 січня 3000 року.
·
java.text.DateFormat. Цей клас використовується для форматування дат і не
тільки забезпечує варіанти стилів типу "01/01/70" або "січень,
1, 1970," але також форматування дат у стилях, прийнятих для багатьох
країн світу.
·
java.text.NumberFormat. Цей клас використовується для форматування чисел і
валют у стилях, прийнятих для багатьох країн світу.
·
java.util.Locale. Цей клас використовується для зв’язку між DateFormat і NumberFormat для форматування дат, чисел і валют у стилях,
прийнятих для багатьох країн світу. З допомогою класу Locale ви зможете миттєво перетворити представлення дати
"10/08/2005" в "Segunda-feira, 8 de Outubro de 2005". При
необхідності маніпулювати датами без виводу відформатованого результату можна
використати клас Locale безпосередньо з класом Calendar.
Організація класів, орієнтованих для роботи із числами
та датами
При роботі із датами і числами, часто використовуються разом декілька
класів одночасно.
Важливо розуміти взаємодію вищеописаних класів і використання
комбінації класів. Для прикладу, при форматуванні дати для специфічного
місцерозташування потрібно створити Locale
об’єкт перед об’єктом DateFormat, тому
що об’єкт Locale виступає
аргументом для методів DateFormat. В
таблиці 6.2 наведено короткий огляд основних випадків, що трапляються при
роботі із датами та числами, а також шляхи відповідного використання класів,
орієнтованих на роботу із датами і числами. Таблиця 6.2 викличе ряд специфічних
запитань, що стосуватимуться конкретних класів, специфікації яких будуть
наведено пізніше. Таблиця 6.2 є підсумком попередньої дискусії про рівні
класів.
Таблиця 6.2 Загальні випадки при роботі із датами та
числами
Випадок
|
Кроки
|
Отримати поточну дату і час
|
1. Створити Date: Date d = new Date();
2. Отримати її значення:
String s = d.toString();
|
Отримати об’єкт для обчислення дати і часу у специфічній локації
|
1. Створити Calendar:
Calendar c = Calendar.getInstance();
2. Використати c.add(...) і c.roll(...) для маніпуляцій з датою і часом
|
Отримати об’єкт для
обчислення дати і часу у іншій локації
|
1. Створення Locale:
Locale loc = new
Locale(language);
Locale loc = new Locale(language,
country);
2. Створення Calendar для відповідної локації:
Calendar c =
Calendar.getInstance(loc);
3. Використати c.add(...) і c.roll(...) для маніпуляцій з датою і часом.
|
Отримати об’єкт для обчислення дати і часу та форматування їх для виводу в різних
локаціях з різними стилями дати
|
1. Створити Calendar:
Calendar c = Calendar.getInstance();
2. Створити Locale для кожної локації:
Locale loc = new
Locale(...);
3. Конвертувати Calendar в Date:
Date d = c.getTime();
4. Створити DateFormat для кодного Locale:
DateFormat df =
DateFormat.getDateInstance (style, loc);
5. Використати метод format() для створення
форматованих дат:
String s = df.format(d);
|
Отримати об’єкт для паралельного форматування числа і валюти в різних
локаціях
|
1. Створити Locale для кожної локації:
Locale loc = new
Locale(...);
2. Створити NumberFormat:
NumberFormat nf =
NumberFormat.getInstance(loc);
NumberFormat nf =
NumberFormat.getCurrencyInstance(loc);
3.Використати
метод format() для створення
форматованого виводу:
String s =
nf.format(someNumber);
|
Клас Date
Клас Date має чорно-біле минуле. Його API дизайн не
призначений для інтернаціоналізації завдань. В даний час більшість методів
класу вилучені, і для більшості задач доцільніше користуватись класом Calendar замість класу
Date. Клас Date присутній в екзамені через декілька причин: його традиційний код є
дійсно легким для швидкого знаходження поточної дати і часу, універсального
часу, що не залежить від часових зон, і, нарешті, він є тимчасовим містком для
форматування об’єкту Calendar із
використанням класу DateFormat.
Як вже згадано вище, екземпляр класу Date представляє відлік дати і часу, що збережені як
примітиви типу long. Особливістю є те, що long вміщує кількість мілісекунд (в одній секунді
міститься 1000 мілісекунд) між заданою датою і першим січня 1970 року.
Для того, щоб збагнути наскільки дійсно великими є ці числа, використаємо
клас Date для визначення періоду в трильйон мілісекунд,
починаючи з 1 січня, 1970:
import java.util.*;
class TestDates {
public static
void main(String[] args) {
Date d1 =
new Date(1000000000000L); // трильйон!
System.out.println("1st date " + d1.toString());
}
}
На JVM із
американською локацією сформується вивід:
1st date Sat Sep 08 19:46:40 MDT 2001
Тобто, для майбутніх справок запам’ятаємо, що трильйон мілісекунд
збігає за кожні 31 і 2/3 роки.
Незважаючи на те, що більшість методів Date's видалені, дотепер можна використати методи getTime і setTime
, хоча і не без складнощів.
Додамо 1 годину до
екземпляру d1 об’єкту Date з попереднього прикладу:
import java.util.*;
class TestDates {
public static
void main(String[] args) {
Date d1 = new Date(1000000000000L); // трильйон!
System.out.println("1st date " +
d1.toString());
d1.setTime(d1.getTime() + 3600000); //
3600000 мс/год.
System.out.println("new time " + d1.toString());
}
}
що видає на тій самій
JVM:
1st date Sat Sep 08 19:46:40 MDT 2001
new time Sat Sep 08 20:46:40 MDT 2001
Відмітимо, що методи setTime() і getTime() використовували доступну мілісекундну шкалу. Таким
чином, маніпулювання датами на основі класу Date не є найкращим вибором, враховуючи розмірність чисел,
що використовуються, наприклад, при додаванні
року до заданої дати.
До класу Date
повернемось пізніше ще раз, але
зараз необхідно запам’ятати єдину річ: при створенні екземпляру Date, що представляє поточний момент,
застосовується конструктор без аргументів:
Date
now = new Date();
(При виклику
now.getTime(), отримується число в межах між одним і двома трильйонами).
Клас Calendar
Маніпулювання датами в класі Date є непростим. Клас Calendar
спроектований для полегшення цих маніпуляцій, для чого має близько мільйона
полів і методів.
Клас Calendar є абстрактним, тобто не можна
використовувати:
Calendar c = new Calendar(); //неправильно, Calendar абстрактний
В порядку створення екземпляру Calendar , ви маєте використовувати один з перегружених статичних
методів getInstance():
Calendar
cal = Calendar.getInstance();
У вищенаведеному прикладі об’єктна змінна cal типу Calendar
буде відноситись тільки до екземпляру конкретного підкласу Calendar. Наперед точно невідомо, який підклас
отримується (при використанні класу java.util.GregorianCalendar гарантія отримання цього класу є стопроцентною), але
це не є важливим для використання Calendar's API. (Оскільки Java продовжує поширюватись по всьому світу, в порядку
підтримання цілісності існують додаткові, локально-специфічні підкласи Calendar.)
Для екземпляру Calendar з попереднього прикладу дізнаємося на який день
тижня припадає наша трильйонна мілісекунда, і потім додамо місяць до цієї дати:
import java.util.*;
class Dates2 {
public static
void main(String[] args) {
Date d1 =
new Date(1000000000000L);
System.out.println("1st date " + d1.toString());
Calendar c =
Calendar.getInstance();
c.setTime(d1); // #1
if(c.SUNDAY == c.getFirstDayOfWeek()) // #2
System.out.println("Sunday is the first day of the week");
System.out.println("trillionth milli day of week is "
+ c.get(c.DAY_OF_WEEK)); // #3
c.add(Calendar.MONTH, 1); // #4
Date d2 = c.getTime(); // #5
System.out.println("new date " + d2.toString() );
}
}
Один з варіантів виведення:
1st date Sat Sep 08 19:46:40 MDT 2001
Sunday is the first day of the week
trillionth milli day of week is 7
new date Mon Oct 08 20:46:40 MDT 2001
Детально
проаналізуємо п’ять виділених лінійок:
1.Завдання екземпляру с типу
Calendar
значення d1 типу Date.
2. Використання поля SUNDAY
класу Calendar для встановлення у JVM поля SUNDAY як першого дня тижня. (В деяких країнах поле MONDAY є першим днем тижня). Клас Calendar забезпечує подібні поля для днів тижня,
місяців, дня місяця, дня року, тощо.
3.Використання поля DAY_OF_WEEK для визначення, на який день тижня припадає трильйонна мілісекунда.
4. В попередніх методах встановлення (setter) та отримання (getter) можна інтуїтивно розібратися. Потужний метод add() класу Calendar дозволяє додавати або віднімати будь-які зручні
одиниці часу, притаманні полям класу Calendar. Для прикладу:
c.add(Calendar.HOUR,
-4); // віднімає 4 години від значення
с
c.add(Calendar.YEAR,
2); // додає 2 роки до значення с
c.add(Calendar.DAY_OF_WEEK, -2); // забирає 2 дні відзначення с
5. Перетворення
значення c до стандартного представлення класу Date.
Метод roll() класу Calendar також необхідно знати для екзамену. Метод roll() діє аналогічно методу add(),
за виключенням того, що коли частина Date інкрементується або декрементується, старша частина Date не змінюється. Наприклад:
// assume c is October 8, 2001
c.roll(Calendar.MONTH, 9); // записує рік у виводі
Date d4 = c.getTime();
System.out.println("new date " +
d4.toString() );
Приблизний вивід:
new date Fri Jul 08 19:46:40 MDT 2001
Значення року не змінилось, незважаючи на додавання 9 місяців до дати у
жовтні! Аналогічним чином, застосування roll() з полем HOUR
не змінить дату, місяць чи рік.
Для екзамену вам не прийдеться запам’ятовувати поля класу Calendar. Якщо ці поля будуть потрібні для відповіді
на запитання, вони будуть вказані, як частина запитання.
Клас DateFormat
Після створення дат і маніпулювання ними дізнаємось, як форматувати їх.
Це все робиться на одній-єдиній сторінці. Приклад форматування дати різними
шляхами:
import java.text.*;
import java.util.*;
class Dates3 {
public static
void main(String[] args) {
Date d1 =
new Date(1000000000000L);
DateFormat[]
dfa = new DateFormat[6];
dfa[0] =
DateFormat.getInstance();
dfa[1] =
DateFormat.getDateInstance();
dfa[2] =
DateFormat.getDateInstance(DateFormat.SHORT);
dfa[3] =
DateFormat.getDateInstance(DateFormat.MEDIUM);
dfa[4] =
DateFormat.getDateInstance(DateFormat.LONG);
dfa[5] =
DateFormat.getDateInstance(DateFormat.FULL);
for(DateFormat df : dfa)
System.out.println(df.format(d1));
}
}
Отриманий вивід:
9/8/01 7:46 PM
Sep 8, 2001
9/8/01
Sep 8, 2001
September 8, 2001
Saturday, September 8, 2001
Аналіз цього коду демонструє декілька цікавих речей. По-перше,
клас DateFormat є абстрактним класом, отже не є можливим
використовувати оператор new для створення екземплярів DateFormat. В такому випадку використано два стандартні
методи - getInstance() і getDateInstance(). Відмітимо, що метод getDateInstance() є перевантаженим; при обговоренні локалей
буде розглянуто іншу версію getDateInstance(), яку потрібно розуміти для екзамену.
По-друге, використано статичні поля класу DateFormat для специфічного налаштування різних
екземплярів DateFormat. Кожне з цих статичних полів представляє
стиль (style) форматування. Для даного випадку версія методу
getDateInstance() без аргументів представляє такий самий стиль,
як і версія MEDIUM, але це не важке і швидке правило (більше про
це буде сказано під час обговорення локалей).
Нарешті, використано метод format()
для створення строк, що представляють собою форматовану версію Date.
Наступний потрібний метод - метод parse(). Метод parse()
конвертує в об’єкти Date строки String, відформатовані згідно із стилем раніше
наведеного об’єкту DateFormat. У
випадку отримання неправильно відформатованого String метод parse()
викликає виключення ParseException.
Наступний код створює екземпляр Date,
використовує DateFormat.format() для конвертації у String, і потім застосовує DateFormat.parse() для зворотнього перетворення назад у Date:
Date
d1 = new Date(1000000000000L);
System.out.println("d1 = " + d1.toString());
DateFormat
df= DateFormat.getDateInstance(DateFormat.SHORT);
String s =
df.format(d1);
System.out.println(s);
try {
Date d2 =
df.parse(s);
System.out.println("parsed = " + d2.toString());
} catch
(ParseException pe) {
System.out.println("parse exc"); }
із виведенням:
d1 = Sat Sep 08 19:46:40 MDT 2001
9/8/01
parsed
= Sat Sep 08 00:00:00 MDT 2001
За рахунок використання стилю SHORT втрачено точність при конвертації Date у String:
при зворотньому перетворенні назад у об’єкт Date час 7:46:40 перетворився
у 00:00:00.
В API для методу DateFormat.parse() пояснено, що за замовчуванням
метод parse() не
є однозначним при роботі з датами. Вищенаведений приклад демонструє, що parse() є дуже неоднозначним при
форматуванні Strings для
подальшого перетворення в дати; необхідна уважність при використанні цього
методу!
Ceртифікаційне завдання. Серіалізація (Exam Objective 3.3)
3.3 Розробити код,
що серіалізує та/або
десеріалізує об’єкти із використанням наступних засобів API з пакету java.io: DataInputStream,
DataOutputStream, FilelnputStream,
FileOutputStream, ObjectInputStream, ObjectOutputStream тa Serializable.
Припустимо, необхідно зберегти
стан одного або декількох об’єктів. В попередніх версіях Java, де серіалізація ще була відсутня, необхідно було використати один з I/O класів для запису стану змінних всіх об’єктів, призначених для зберігання.
Найгірша частина намагатиметься реконструювати нові об’єкти, віртуально
ідентичні тим, що мають бути збереженими. Для збереження та відновлення стану
кожного об’єкту потрібен специфічний протокол, інакше можна в кінцевому підсумку отримати
змінні із неправильними значеннями. Наприклад, припустимо, що в об’єкті
зберігаються змінні для висоти та ваги. Під час збереження стану об’єкту значення змінних
записуються до файлу у вигляді двох ints, причому порядок запису є важливим. Відновити об’єкт виглядає занадто
простим завданням, проте переплутування
значень висоти та ваги може в новому об’єкті призвести до взаємного обміну
значень цих параметрів.
Серіалізація просто вказує "зберегти цей
об’єкт та всі його змінні". Насправді, це дещо цікавіше, тому що можна додати "… тим не менше,
значення змінної, явно позначеної модифікатором transient, не вважається
частиною стану об’єкту, що серіалізується".
Основна серіалізація насправді
здійснюється двома методами: один серіалізує
об’єкти та записує їх до потоку, другий читає потік та десеріалізує об’єкти:
ObjectOutputStream.writeObject() // серіалізує та записує
ObjectInputStream.readObject() // читає та десеріалізує
Класи java.io.ObjectOutputStream тa java.io.ObjectInputStream вважаються високорівневими класами у пакеті java.io, тобто, можуть
бути накрученими навколо низькорівневих
класів типу java.io.FileOutputStream тa java.io.FilelnputStream. Нижче
наведено маленьку програму, що створює об’єкт (Cat), серіалізує його, а потім
десеріалізує:
import
java.io.*;
class Cat implements Serializable { } // 1
public class
SerializeCat {
public static void main(String[] args) {
Cat
c = new Cat(); // 2
try {
FileOutputStream fs = new
FileOutputStream("testSer.ser");
ObjectOutputStream os = new
ObjectOutputStream(fs);
os.writeObject(c); // 3
os.close();
} catch (Exception e) { e.printStackTrace
(); }
try {
FileInputStream fis = new
FileInputStream("testSer.ser");
ObjectInputStream ois = new
ObjectInputStream(fis);
c
= (Cat) ois.readObject(); // 4
ois.close();
} catch (Exception e) {
e.printStackTrace(); }
}
}
Розглянемо ключові моменти
прикладу:
1. Задекларовано,
що клас Cat реалізує інтерфейс Serializable. Serializable є маркованим інтерфейсом; він не має
методів для реалізації. (В декількох наступних параграфах
буде розглянуто різноманітні правила для декларування класів як Serializable.)
2. Створено новий
об’єкт Cat, про який
відомо, що він є Serializable.
3.
Об’єкт Cat серіалізовано
викликом методу writeObject(). Перед дійсною серіалізацією Cat виконується цілий ряд
приготувань. По-перше, код, пов’язаний із
введенням-виведенням, розміщається в блоці try/catch. Потім створюється FileOutputStream для запису до нього об’єкту. Далі FileOutputStream загортається у ObjectOutputStream, який має
необхідний метод серіалізації. Пам’ятайте, що виклик writeObject() виконує два завдання: серіалізації об’єкту та запису серіалізованого об’єкту до файлу.
4.
Десеріалізація об’єкту Cat здійснюється викликом методу readObject(), що повертає Object, отже необхідне
зворотнє перетворення десеріалізованого об’єкту до Cat.
Це був схематичний приклад дії
серіалізації. Далі наведено більш комплексні питання, пов’язані із
серіалізацією.
Що насправді означає збереження
об’єкту? Якщо змінні
екземпляру є виключно примітивами, це здійснюється доволі просто. А якщо змінні екземпляру є
посиланнями на об’єкти? Що дасть
збереження? В Java немає змісту
зберігати дійсні значення змінної посилання, тому що значення Java посилання
означає тільки внутрішній зміст єдиного екземпляру JVM. Iншими словами, посилання є
зайвим при намаганні зберегти об’єкт у іншому екземплярі JVM, навіть якщо
виконання здійснюється на тому самому комп’ютері, де відбувалась серіалізація.
Але якщо об’єкт посилається на
інший об’єкт? Розглянемо клас:
class Dog {
private Collar theCollar;
private int dogSize;
public Dog(Collar collar, int size) {
theCollar = collar;
dogSize = size;
}
public Collar getCollar() { return
theCollar; }
}
class Collar
{
private int collarSize;
public Collar(int size) { collarSize = size;
}
public int getCollarSize(} { return
collarSize; }
}
Зробимо Dog… Спочатку,
зробимо Collar для Dog:
Collar c =
new Collar(3);
Тепер зробимо новий Dog, передаючи йому Collar:
Dog d = new
Dog(c, 8);
Що відбувається при збереженні Dog? Якщо метою є збереження, а потім
відновлення Dog, тa відновлений Dog є точною копією
збереженого Dog, тоді для Dog потрібний Collar, що є точною
копією Collar класу Dog в той час, коли Dog зберігався. Це означає, що
необхідно зберігати як Dog, так і Collar.
А якщо, в свою чергу, Collar також
посилається на інші об’єкти? Ситуація ускладнюється дуже швидко. Програміст повинен зберігати стани всіх об’єктів, на які посилаються
об’єкти, що входять до складу внутрішньої структури Dog. Це стає кошмаром навіть для
найпростіших об’єктів.
На щастя, механізм серіалізації в Java турбується про
все це. При
серіалізації об’єкту, Java турбується про
збереження всіх об’єктів у цілому "графі об’єкту". Це означає глибоке копіювання всіх об’єктів, необхідних для збереження. Наприклад, при
серіалізації об’єкту Dog об’єкт Collar буде серіалізовано автоматично. A якщо клас Collar містить
посилання на інші об’єкти, ЦЕЙ об’єкт також буде серіалізовано, тощо. Єдиним об’єктом, про який слід потурбуватись
для збереження та відновлення є Dog. Інші об’єкти, необхідні для відновлення Dog, зберігаються та відновлюються
автоматично при серіалізації.
Пам’ятайте,
створення серіалізованого об’єкту реалізацією інтерфейсу Serializable є усвідомленим вибором. Якщо необхідно зберегти об’єкти Dog, наприклад, потрібно
модифікувати клас Dog наступним чином:
class Dog implements Serializable {
//залишки попереднього коду
Ідентифікатори та JavaBeans (Exam Objectives 1.3, 1.4)
1.3. Розробити
код, що оголошує, ініціалізує та використовує примітиви, масиви, перерахування (enums) та об’єкти, в тому числі статичні, а також локальні змінні та змінні
екземплярів. Використовуйте правильні ідентифікатори для імен змінних.
1.4. Розробити
код, що оголошує статичні та нестатичні методи, і – якщо треба, - використовує
назви методів, які дотримуються стандартів найменування JavaBeans. Також розробити код, що оголошує та використовує список
аргументів змінної довжини (var-args).
При вказуванні одного або більшої кількості
сертифікаційних завдань в розділі, як це зроблено зараз, гарантовано, що в цьому
розділі розглянуто принаймні частину цих завдань. Деякі завдання розглядаються
в кількох розділах, тому їх можна побачити у багатьох місцях книги. Для
прикладу, в цьому розділі розглянуто оголошення, ідентифікатори та найменування
JavaBeans, але більшу частину цих завдань описано в інших розділах.
Розглянемо
три аспекти ідентифікаторів Java:
-
Правильність
ідентифікаторів. Правила, за якими компілятор визначає
правильність імені.
-
Sun конвенції для Java коду. Рекомендації Sun фірми
для найменування класів, змінних і методів. Ці стандарти використано на протязі
всієї книги, за винятком тих випадків, коли необхідно показати нестандартність
питання екзамену. Питання Sun конвенції відсутні на екзамені, але потрібні при
написанні програм.
-
Стандарти найменування JavaBeans. Вимоги до найменувань
специфікації JavaBeans. На екзамені потрібно знати декілька найпростіших
правил, які розглянуто в цьому розділі.
Правильні ідентифікатори
Ідентифікатори можуть складатись тільки з символів
Unicode, цифр, символів валюти та символу «_». Екзамен не вникає
в деталі, які типи символів Unicode класифікуються цифрами, а які
літерами. Тому, наприклад, не потрібно знати, що тібетські цифри знаходяться в
діапазоні з \u0420 по \u0f29. Ось необхідні правила,
які потрібно знати:
-
Ідентифікатори мають починатися з літери, знаку валюти,
або з’єднуючого символу. Вони НЕ можуть починатись з цифри!
-
Після першого символу ідентифікатори можуть містити
будь-яку комбінацію літер, цифр та інших символів.
-
На практиці немає обмежень кількості символів в
ідентифікаторі.
-
Не можна використовувати ключові слова Java як ідентифікатори.
Таблиця 1-1 перелічує всі ключові слова, включно із enum - новим від Java5.0,.
-
Ідентифікатори в Java розрізняють великі і малі букви:
foo і FOO –
це два різні ідентифікатори.
Нижче наведено приклади правильних і неправильних
ідентифікаторів, спочатку правильні:
int _a;
int $c;
int ______2_w;
int _$;
int
this_is_a_very_detailed_name_for_an_identifier;
А ці – неправильні (подумайте, чому):
int :b;
int -d;
int e#;
int .f;
int 7g;
Таблиця 1-1: повний перелік ключових слів Java:
Sun конвенції Java коду
Спеціалісти Sun підрахували, що на власне
створення і тестування коду витрачається 20% часу, а 80% - та підтримку цього
коду. Згідно з цим, набір стандартів коду допомагає зменшити час відлагодження,
підтримку та тестування кожного фрагменту
коду. Спеціалісти Sun створили набір стандартів Java коду і опублікувала їх в
документі з назвою «Конвенції коду Java», який розміщено на java.sun.com. Це прекрасний короткий документ,
який добре читається, тому ми його рекомендуємо.
Вже було сказано, що багато питань на екзамені не
відповідають цим конвенціям через обмеження в засобах тестування, що
використовуються для міжнародної верифікації екзамену. Один із плюсів
сертифікації в тому, що екзамени виконуються однаково в усьому світі. Отож, лістинги
кодів на реальному екзамені часто є насправді обмеженими і не відповідають
стандартам кодування Sun. Оскільки завданням є підготовка
до екзамену, ми часто будемо представляти лістинги коду, в яких використано 2 пробіли,
на противагу 4 пробілам у стандартах Java.
Ми також будемо неприродним чином стискати круглі
дужки разом, а інколи - розміщати
декілька виразів в один рядок. Для прикладу:
1. class Wombat
implements Runnable {
2. private int i;
3. public synchronized void run() {
4. if
(i%5 != 0) { i++; }
5. for(int x=0; x<5; x++, i++)
6. { if (x > 1) Thread.yield(); }
7. System.out.print(i + " ");
8. }
9. public static void main(String[] args) {
10. Wombat n = new Wombat();
11. for(int x=100; x>0; --x){new Thread(n).start();}
12. } }
Наперед попереджаємо – ви побачите багато лістингів
коду, нестандартних питань, а також питань з реального екзамену, які будуть
закрученими. Ніхто не хоче, щоб ви писали код, подібний наведеному. Ані
роботодавець, ані колеги, ані ми, ані Sun, ані команда розробки
питань для екзамену! Код, подібний наведеному, створений виключно для того, щоб
складні вирази могли тестуватись універсальним тестовим інструментом. Єдиний
стандарт, який зберігається, наскільки це можливо, в реальному екзамені, – це
стандарти найменувань. Нижче наведено стандарти, рекомендовані Sun, які використано на
екзамені і, в більшості випадків, в цій книзі.
-
Класи та інтерфейси. Перша літера має бути
великою, і якщо кілька слів з’єднано у формі імені, перша літера внутрішніх
слів теж має бути великою (цей формат деколи називається «сamelCase». Імена класів, зазвичай, повинні бути
іменниками. Наприклад:
Dog
Account
PrintWriter
Імена
інтерфейсів повинні бути прикметниками, типу:
Runnable
Serializable
-
Методи. Перша літера має бути
малою, а потім застосовується правило camelCase. Крім того, імена, зазвичай, складаються з пари іменник-дієслово.
Наприклад:
getBalance
doCalculation
setCustomerName
-
Змінні. Як і в методах
застосовується формат camelCase, але починаються з маленької
літери. Sun рекомендує короткі,
визначені імена, що звучать добре для нас. Приклади:
buttonWidth
accountBalance
myString
-
Константи. Створюються
позначеннями змінних static і final. Повинні використовувати великі літери із використанням символу «_» як
розділювача:
MIN_HEIGHT
Стандарти JavaBeans
Дана специфікація призначена для допомоги
розробникам Java в створенні компонентів, що можуть легко використовуватись
іншими розробниками в візуальному інтегрованому середовищі розробника (IDE)
(наприклад, Eclipse або NetBeans). Програмісту Java
зручно використовувати компоненти з Java
API, але не менш зручним є купити потрібні компоненти від "Beans
'R Us" -
сусідньої компанії по розробці програмного забезпечення. В такому випадку потрібно
мати доступ до компонентів через IDE, щоби не знадобилось ще раз
переписувати код. При використанні правил найменування специфікація JavaBeans гарантує, що IDE
розпізнаватимуть і використовуватимуть компоненти, створені різними
розробниками. JavaBeans APІ досить широка, але для екзамену потрібно вивчити
тільки декілька простих правил.
По-перше, JavaBeans – це класи Java, які
мають властивості (properties). Для наших завдань це подібне змінним private, тобто, єдиним шляхом доступу до
них є методи даного класу. Методи, що можуть змінювати значення властивості,
називаються setter-методами, а методи, що отримують
ці значення – getter-методами. Необхідно знати наступні правила найменувань
властивостей JavaBeans :
-
якщо властивість не є булевою, префікс getter-методу має бути get. Наприклад, getSize() є коректним JavaBeans іменем getter-методу для властивості "size". Не обов’язково називати змінну саме size (хоча деякі IDE
очікують цього). Імя властивості визначається setter- та getter-методами, але без використання змінних класу. Що поверне getSize() – залежить виключно від програміста.
-
якщо властивість є булевою, префікс методу може бути як get,
так і is. Наприклад, getStopped() і isStopped() є коректними JavaBeans іменами для булевої властивості.
-
префікс setter-методу має бути set. Наприклад,
setSize() є коректним JavaBeans іменем для властивості size.
-
для утворення імені setter- та getter-методів необхідна зміна першої літери назви властивості на велику і додавання
до неї потрібного префіксу (get, is або set).
-
сігнатури setter-методів повинні бути позначені public, мати змінну повертання та аргументи, відповідні до типу властивості.
-
сігнатури getter-методів повинні бути позначені public, не мати аргументів, і мати змінну
повертання, відповідну до типу аргументу setter-методу цієї властивості.
По-друге, специфікація JavaBeans підтримує події, що дозволяє компонентам повідомляти
один одному про відбування події. Модель подій часто використовується в програмах
GUI, коли подія типу натискання кнопки миші є сигналом для
багатьох об’єктів, що мають завдання на цей випадок. Об’єкти, що отримують
інформацію при відбуванні події, називаються слухачами. Для екзамену потрібно знати, що методи, що
використовуються для додавання або видалення слухачів з події, мають також виконувати
наступні стандарти найменувань слухачів JavaBeans:
-
Метод слухача, що використовується для реєстрації слухача
із джерелом події, має використовувати префікс add, за яким знаходиться тип
слухача. Наприклад, addActionListener() є коректним іменем для методу, в
якому джерело подій дозволить іншим реєстрацію подій Action.
-
Імена методу для видалення слухача мають використовувати
префікс remove, за яким знаходиться тип слухача) (використовуються
однакові правила, що й для add методів реєстрації).
-
Тип слухача, щоб бути доданим або видаленим, має бути переданим
в якості аргументу методу.
-
Методи слухачів мають закінчуватися словом "Listener".
Приклади JavaBeans коректних сигнатур методів:
public
void setMyValue(int v)
public
int getMyValue()
public
boolean isMyStatus()
public
void addMyListener(MyListener m)
public
void removeMyListener(MyListener m)
Приклади JavaBeans некоректних сигнатур методів:
void
setCustomerName(String s) //повинен бути public
public void
modifyMyValue(int v)
// не може використовувати
'modify'
public void
addXListener(MyListener m)
// не підходить тип слухача
Увага для екзамену. Сертифікаційні завдання вимагають знання
правильних ідентифікаторів тільки для імен змінних, але правила є аналогічними
для ВСІХ компонентів Java. Тобто, правильний ідентифікатор для змінної буде
правильним також для методу чи класу. Потрібно розрізняти коректні
ідентифікатори і конвенції найменувань, наприклад, стандарти JavaBeans, де наведено правила для назв компонентів Java. Іншими словами,
потрібно розпізнавати, що ідентифікатор є коректним навіть, якщо він не
відповідає стандартам найменування. Якщо питання екзамену стосується конвенцій найменувань
– не тільки, коли ідентифікатор буде компілюватись –JavaBeans мають бути вказані безпосередньо.
Служебные
слова (keywords)
не могут применяться в качестве идентификаторов, поскольку правила их
употребления строго регламентированы в самом языке. В таблице, приведенной
ниже, перечислены все служебные слова Языка программирования Java (слова,
отмеченные символом t, зарезервированы, но не используются).
abstract
|
default
|
if
|
private
|
this
|
boolean
|
do
|
implements
|
protected
|
throw
|
break
|
double
|
import
|
public
|
throws
|
byte
|
else
|
instanceof
|
return
|
transient
|
case
|
extends
|
int
|
short
|
try
|
catch
|
fi nal
|
interface
|
static
|
void
|
char
|
finally
|
long
|
strictfp
|
volatile
|
class
|
float
|
native
|
super
|
while
|
constt
|
for
|
new
|
switch
|
|
continue
|
gotot
|
package
|
synchronized
|
null, true и false, хотя и выглядят как служебные слова,
формально относятся к числу литералов, таких как, скажем, постоянная величина
12, и поэтому в таблицу не включены. Однако их нельзя применять в качестве
идентификаторов – точно так же, как и число 12. Впрочем, null, true и false
вполне допустимо использовать в составе других слов-идентификаторов, например
annulled, construe или falsehood.
Немає коментарів:
Дописати коментар