Пробуждането на Тигъра<br>Новите възможности в J2SE 5.0

Категория: Интернет
вторник, 10 Февруари 2004 0:00ч

На 30 септември 2004 г. Sun Microsystems официално обяви излизането на новата версия 5.0 на Java 2 Platform Standard Edition (J2SE), позната под кодовото име Тигър.
За почти четири години работа по J2SE 5.0 повече от 150 експерти са участвали в изграждането на новия облик на платформата с акцент върху леснотата на ползване, общата производителност, разширяемостта и управлението.
Два факта около създаването и официалното обявяването на Тигър са особено любопитни. Първо, това е може би най-модифицираната до момента версия на спецификацията (над 100 мащабни нововъведения и доста по-незначителни, както и такива, засягащи самия език Java). и второ - процесът на разработка е бил сериозно подпомогнат от Java общността (над 4 000 получени писма и 25 000 реда код са добавени от ентусиасти).
Промените в J2SE засягат множество аспекти на платформата - производителността, самия език Java, вируталната машина, базовите библиотеки и тези, обслужващи интеграционните услуги, потребителския интерфейс, различните инструменти и поддържани архитектури.
Разбира се, най-основни в ежедневието на разработчиците са промените, касещи синтаксиса и семантиката на езика. Тук ще представя накратко най-интересните промени и нововъведения именно в самия език Java съгласно новата спецификация Тигър.

Параметризирани типове
Обикновено за илюстрация на типизация се използват контейнерни типове. Интерфейсът Collection например, който стои в основата на цяла йерархия от класове/интерфейси, реализиращи контейнери за данни, е имплементиран чрез подинтерфейсите си в най-популярните структури (например Vector, ArrayList и т.н.) е променен с цел поддръжка на параметризирани типове. Нека разгледаме следния пример:
1: public void fillData() {
2:
3: List data = new ArrayList();
4:
5: data.add(new String(John Keats));
6: data.add(new String(The Eve of St. Agnes));
7: data.add(new Integer(1819));
8:
9: displayData(data);
10:
11: }
Тук в ArrayList обекта data записваме данни за литературни произведения, чийто формат е автор, произведение, година на написване. Тъй като по време на изпълнение всички колекции са колекции, съдържащи елементи от тип Object, не е проблем да предадем на метода add два обекта от тип String и един от тип Integer (редове 5 и 7). Затруднението настъпва, когато обектите трябва да бъдат извлечени и записани в някаква променлива.
1: public void displayData(Collection col) {
2:
3: Iterator it = col.iterator();
4:
5: while (it.hasNext()) {
6:
7: String element = (String) it.next();
8:
9: System.out.println(element);
10:
11: }
12: }
Изпълнението на горния код след извикване на fillData() ще доведе до ClassCastException на ред 7, когато процесът достигне до обработката на Integer данната. Използвайки новата възможност за параметризация на типовете, разглеждания пример може да се напише така:
1: public void fillData() {
2:
3: List data = new ArrayList();
4:
5: data.add(new String(John Keats));
6: data.add(new String(The Eve of St. Agnes));
7: data.add(new Integer(1819));
8:
9: displayData(data);
10:
11: }
12:
13: public void displayData(Collection col) {
14:
15: Iterator it = col.iterator();
16:
17: while (it.hasNext()) {
18:
19: String element = it.next();
20:
21: System.out.println(element);
22:
23: }
24: }
Приема се, че записът List се чете като List от тип String. Така направената типизация информира компилатора за типа на данните, които ще държим в структурата, за да може той да проверява тяхната валидност.
При опит за компилация на така преправения код ще се получи съобщение за грешка, информиращо, че не може да се добавят елементи от тип Integer в колекция от тип String.
Използването на показаната (ред. 3 и 15) параметризация гарантира проверката на типовете във фазата на компилация и премества откриването на такива грешки към по-ранна фаза от процеса на изпълнение. Освен възползване от параметризацията на различни класове (като ArrayList), възможно е параметризирането и на собствени такива.
За да може един клас да поддържа параметризация, то неговата декларация трябва да бъде леко изменена, както и да се вземат някои допълнителни мерки в методите му. Нека за пример погледнем извадка от кода на интерфейса List:
1: public interface List {
2:
3: void add(E x);
4:
5: Iterator iterator;
6:
7: ...
8:
9: }
Тук Е е формален параметър за типа и може да се използва почти както един обичаен тип (включително и за указване на типа на аргумент на метод, както е видно). Ако се върнем към показания пример с авторите, то по време на компилация на мястото на E ще застане действителният тип на аргумента - String.
Параметризираните типове са изключително полезни - те помагат да се създава обща функционалност за множество типове данни, които могат да се проверяват за типова безопасност по време на изпълнение. От гледна точка на производителността тази нова функционалност не оказва съществено влияние, тъй като обработките, от които се нуждае, се извършват предимно във фазата на компилация.

Цикли
Основното подобрение на цикъла for е неговата възможност да обхожда колекции и масиви, без да е нужно използването на допълнителна променлива (за индекс) или итератор. Ако преправим първоначалния вариант на displayData така, че да използва for цикъл, то би се получило нещо такова:
1: public void displayData(Collection col) {
2:
3: for (Iterator it = col.iterator(); it.hasNext();) {
4:
5: String element = (String) it.next();
6:
7: System.out.println(element);
8:
9: }
10: }
С новия синтаксис горният пример може да се трансформира в по-кратък и ясен код, а именно:
1: public void displayData(Collection col) {
2:
3: for (String str : col) {
4:
5: System.out.println(str);
6:
7: }
8:
9: }
В този случай е прието двуеточието да се чете като в. Или редът for (String str : col) се тълкува като за всеки String str в col.
Използването на това подобрение премахва и една често допускана грешка при вложен обход с итератори, а именно показаната по-долу:
1: List suits = ...;
2: List ranks = ...;
3: List sortedDeck = new ArrayList();
4:
5: for (Iterator i = suits.iterator(); i.hasNext(); )
6:
7: for (Iterator j = ranks.iterator(); j.hasNext(); )
8
9: sortedDeck.add(new Card(i.next(), j.next()));
Лесно се вижда, че този код води до NoSuchElementException на ред 9 заради прекалено многото извиквания на i.next(). В практиката избягването на този проблем често се решава така:
1: for (Iterator i = suits.iterator(); i.hasNext(); ) {
2:
3: Suit suit = (Suit) i.next();
4:
5: for (Iterator j = ranks.iterator(); j.hasNext(); )
6:
7: sortedDeck.add(new Card(suit, j.next()));
8:
9: }
Въпреки това използването на новите възможности на J2SE свеждат ситуацията до едно доста по-елегантно решение:
1: for (Suit suit : suits)
2:
3: for (Rank rank : ranks)
4:
5: sortedDeck.add(new Card(suit, rank));
Същият механизъм може да се използва и за обхождане на масиви. Например за намирането на сумата на елементите на масив от целочислен тип може да се използва следният подход:
1: public int getSum(int array[]) {
2:
3: int sum = 0;
4:
5: for(int i : array) {
6:
7: sum += i;
8:
9: }
10:
11: return sum;
12: }
Една идея, която може да ни хрумне веднага, е, че ако имаме някакъв обхождащ метод, например
1: void printCollection(Collection col) {
2:
3: Iterator it = col.iterator();
4:
5: for (int i = 0; i < col.size(); i++) {
6:
7: System.out.println(it.next());
8:
9: }
10:
11: }
можем да го трансформираме така:
1: void printCollection(Collection col) {
2:
3: for (Object obj : col) {
4:
5: System.out.println(obj);
6:
7: }
8:
9: }
Тук възниква един малък проблем (и нужда от ретроспекция към параметризацията), тъй като трансформираният код (въпреки нашите очаквания) може да бъде извикван само с колекции от тип Object. Това е така поради факта, че при типизацията съществува следното правило: ако клас B e подтип (подклас или подинтерфейс) на клас А и X е обща декларация, то НЕ Е ВЯРНО, че X е подтип на X. Това правило е трудно за приемане, тъй като върви срещу интуитивното ни разбиране за класове и подкласове. Въпреки това то е очевидно, ако разгледаме следния фрагмент:
1: List listOfString = new ArrayList();
2:
3: List listOfObject = listOfString;
4:
5: listOfObject.add(new Object());
6:
7: String str = listOfString.get(0); // Присвояване на Object в String!
За решаването на спомената по-горе ситуация, а именно, ако премахнем ограничението на метода printCollection(), така че той да приема типизирани колекции от всякакъв вид, трябва да използваме специален запис, указващ надтип на всички колекции. Това става с помощта на знака ?, а записът изглежда така:
1: void printCollection(Collection col) {
2:
3: for (Object obj : col) {
4:
5: System.out.println(obj);
6:
7: }
8:
9: }
Ред 3 продължава да е безопасен, понеже какъвто и да е типът на колекцията, тя в крайна сметка съдръжа обекти. Разбира се, все още е некоректно да се добавят произволни обекти в типизирана колекция. Например:
1: Collection col = new ArrayList();
2:
3: col.add(new Object()); // грешка по време на компилация
Всъщност добавянето на елемент в колекция е прелюдия към следващото нововъведение.

Autoboxing/Unboxing
Нуждата от поставянето на примитивните типове в обвиващи класове (boxing), когато трябва да се добавят в колекция например, усложнява разчитането на кода, а и в много случаи е досадно. Тази ситуация е илюстрирана в следващия пример, показващ два метода за поставяне и вземане на елементи от списък (предполагаме, че списъкът list е предварително деклариран и инициализиран):
1: public void addElement(int value) {
2:
3: list.add(new Integer(value));
4:
5: }
6:
7: public int getElement(int index) {
8:
9: return ((Integer) list.get(index)).intValue();
10:
11: }
Използвайки новите възможности за Autoboxing/Unboxing, този код може да се преправи просто на:
1: public void addElement(int value) {
2:
3: list.add(value);
4:
5: }
6:
7: public int getElement(int index) {
8:
9: return list.get(index);
10:
11: }
Важно е да се отбележи обаче, че за да работи коректно, списъкът трябва да бъде типизиран съгласно данните, които ще бъдат обработвани (а именно декларацията и инициализацията на списъка да изглеждат например така : List list = new ArrayList();)
Това нововъдение е изключително полезно, но не е зле да подхождаме внимателно към него. Трябва да се има предвид, че стойността на един Integer обект може да бъде и null. В този случай autounboxing-ът ще генерира NullPointerException. Също така autoboxing/unboxing възможност има негативен ефект върху производителността. Строго се препоръчва тя да не се ползва при тежки изчисления или други чувствителни от гледна точка на производителност части на кода. Освен това не трябва да се забравя, че въпреки че прави неясна разликата между примитивни и референтни типове, тази нова концепция не я премахва и Integer не е заместител на int.

Безопасни изброими типове
Изброимите типове представят множества с краен брой константни стойности. Изброими типове често се използват по следния начин:
1: class LanguageType {
2:
3: public static final int LANG_ ENGLISH = 0;
4:
5: public static final int LANG_ BULGARIAN = 1;
6:
7: public static final int LANG_ DEUTSCH = 2;
8:
9: }
По подобен начин обикновено се дефинират класове, съдържащи константи (в този случай за езиковата настройка на приложението). Този подход има няколко основни недостатъка, най-важните между които са: липсата на пространства за имена, предизвикваща нуждата от префикси (в случая LANG); няма лесен начин константите да се отпечатат, така че да стане ясен техният смисъл; нищо не възпрепятсва инстанцирането или създаването на подкласове на LanguageType, а и като цяло LanguageType нарушава принципите на ООП (един обект от този тип ще има състояние, вместо състояние и поведение).
За избягването на този проблем често се използва еталон за типово безопасни изброими типове, който изглежда така:
1: class LanguageType {
2:
3: private final String displayName;
4:
5: private LanguageType(String displayName) {
6:
7: this.displayName = displayName;
8:
9: }
10:
11: public String toString() {
12:
13: return displayName;
14:
15: }
16:
17: public static final LanguageType ENGLISH = new LanguageType(ENGLISH );
18:
19: public static final LanguageType BULGARIAN = new LanguageType(BULGARIAN );
20:
21: public static final LanguageType DEUTSCH = new LanguageType(DEUTSCH );
22:
23: }
По този начин се реализира типова безопасност - няма как един такъв клас да бъде инстанциран или да се създаде негов наследник. Още повече, ако бъде деклариран метод, приемащ параметър от тип LanguageType, съществува гаранция, че всеки ненулев предаден параметър ще съдържа един от възможните (и коректни) типове. За съжаление предложеното решение също не е съвършено. Между най-големите му недостатъци е невъзможността константите му да се използват в switch конструкции.
За да се избегне излишното създаване на подобни класове (а и свързаните с тях проблеми), в Tiger е добавена нова конструкция, която се справя със задачата доста по-елегантно. А именно :
1: public enum LanguageType {ENGLISH, BULGARIAN, DEUTSCH};
Този кратък запис е достатъчно мощен. Чрез него се реализира типова безопасност на ниво компилация, предоставят се отделни пространства за имена за всеки изброим тип, отпечатването на стойностите дава смислен резултат.
Важно е да обърнем внимание на факта, че в показания пример enum води до конструкцията на изцяло валиден Java клас (а не на прост списък с числени стойности, както е в повечето езици), който при това имплементира интерфейсите Comparable(с типизация ) и Serializable. Класът LanguageType също така притежава три статични променливи - ENGLISH, BULGARIAN и DEUTSCH. Достъпни са и статичните методи values() (който връща масив, съдържащ трите константи), valueOf (String ) (който връща подходящия enum за съответния низ), както и предефинираните equals(), hashCode(), toString() и compareTo().

Коментари

Добави коментар

Име:

Коментар:


 

PCMagazine Брой 2008-04-17Зелените машини :: С надигащата се вълна от притеснение относно замърсяването на околната среда започват да се произвеждат екологично чисти компютри, както и нови технологии, имащи за цел да намалят опасните химикали и употребата на енергия.