Начало J2EE Новите възможности в J2SE 5.0
Новите възможности в J2SE 5.0
Сряда, 10 Септември 2008 07:14
На 30 септември (четвъртък) 2004 г. Sun Microsystems официално обяви излизането на новата версия 5.0 на Java 2 Platform Standard Edition (J2SE) повече позната под кодовото име “Тигър”. 
За почти четири години работа по J2SE 5.0, в процеса на разработка са били включени почти 160 експерти, който са изграждали новия облик на платформата с акцент върху ленотата на ползване, общата производителност, разширяемостта и управлението и т.н.
Два факта около създаването и официалното обявяването на “Тигър“ придизвикват любопитство. Първо – това е може би най-модифициранта до момента версия на спецификацията (с над 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<String> data = new ArrayList<String>();
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<String> it = col.iterator();
16:
17: while (it.hasNext()) {
18:
19: String element = it.next();
20:
21: System.out.println(element);
22:
23: }
24: }

Приема се, че записът List<String> се чете като List “от тип” String. Така направената типизация информира компилатора за типа на данните, които ще държим в структурата, за да може той да проверява тяхната валидност.

При опит за компилация на така преправения код, ще се получи съобщение за грешка, информиращо, че не може да се добавят елементи от тип Integer в колекция от тип String.

Използването на показаната (ред. 3 и 15) параметризация, гарантира проверката на типовете във фазата на компилация и премества откриването на такива грешки към по-ранна фаза от процеса на изпълнение. Освен възползване от параметризацията на различни класове (като ArrayList), възможно е параметризирането и на собствени такива.

За да може един клас да поддържа параметризация, то неговата декларация трябва да бъде леко изменена, както и да се вземат някои допълнителни мерки в методите му. Нека за пример погледнем извадка от кода на интерфейса List:


1: public interface List<E> {
2:
3: void add(E x);
4:
5: Iterator<E> 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<String> 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<Object> col) {
2:
3: for (Object obj : col) {
4:
5: System.out.println(obj);
6:
7: }
8:
9: }

Тук възниква един малък проблем (и нужда от ретроспекция към параметризацията), тъй като трансформирания код (въпреки нашите очаквания) може да бъде извикван само с колекции от тип Object. Това е така, поради факта, че при типизацията съществува следното правило: Ако клас B e подтип (подклас или подинтерфейс) на клас А и X е обща декларация, то НЕ Е ВЯРНО, че X<B> е подтип на X<A>. Това е правило е трудно за приемане, тъй като върви срещу интуитивното ни разбиране за класове и подкласове. Въпреки това, то е очевидно, ако разгледаме следния фрагмент:


1: List<String> listOfString = new ArrayList<String>();
2:
3: List<Object> 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<String>();
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<Integer> list = new ArrayList<Integer>();)

Това нововъдение е изключително полезно, но не е зле да подхождаме внимателно към него. Трябва да се има предвид, че стойността на един 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(с типизация <LanguageType>) и Serializable. Класът LanguageType също така притежава три статични променливи – ENGLISH, BULGARIAN и DEUTSCH. Достъпни са също така и статичните методи values() (който връща масив, съдържащ трите константи), valueOf (String ) (който връща подходящия enum за съответния низ), както и предефинираните equals(), hashCode(), toString() и compareTo().

Един цялостен пример за използването на изборими типове би изглеждал така:


1: public class Example {
2:
3: public enum LanguageType { ENGLISH, BULGARIAN, DEUTSCH }
4:
5: public static void main(String[] args) {
6:
7: for (LanguageType lang : LanguageType.values())
8:
9: System.out.println(lang);
10:
11: }
12:
13: }

В допълнение, за по-добрата поддръжка на параметризирани типове в пакета java.util са добавени и два нови класа – EnumSet и EnumMap. Чрез тях лесно може да се извършват итерации в диапазона на константите. Ако е дефинирана следната структура

enum Month { JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER,
OCTOBER, NOVEMBER, DECEMBER }

то можем да обходим полугодието така

1: for (Month month : EnumSet.range(Month.JANUARY, Month.JUNE))
2:
3: System.out.println(d);

В допълнение, поради класовата характеристика на enum, разработчиците имат възможност да добавят и логика към изброимите типове. Един много интересен пример, показан в официалната документация на Tiger е илюстрацията на enum, съдържащ информация за различните планети от Слънчевата система, като всяка планета пази информация за масата и радиуса си.

1: public enum Planet {
2:
3: MERCURY (3.303e+23, 2.4397e6),
4: VENUS (4.869e+24, 6.0518e6),
5: EARTH (5.976e+24, 6.37814e6),
6: MARS (6.421e+23, 3.3972e6),
7: JUPITER (1.9e+27, 7.1492e7),
8: SATURN (5.688e+26, 6.0268e7),
9: URANUS (8.686e+25, 2.5559e7),
10: NEPTUNE (1.024e+26, 2.4746e7),
11: PLUTO (1.27e+22, 1.137e6);
12:
13: private final double mass; // в килограми
14: private final double radius; // в метри
15:
16: Planet(double mass, double radius) {
17:
18: this.mass = mass;
19: this.radius = radius;
20:
21: }
22:
23: private double mass() { return mass; }
24: private double radius() { return radius; }
25:
26: // универсална гравитационна константа (m3 kg-1 s-2)
27: public static final double G = 6.67300E-11;
28:
29: double surfaceGravity() {
30:
31: return G * mass / (radius * radius);
32:
33: }
34:
35: double surfaceWeight(double otherMass) {
36:
37: return otherMass * surfaceGravity();
38:
39: }
40:
41: }

Следното примерно използване на Planet с подходящ параметър (съгласно примера – 175)

1: public static void main(String[] args) {
2:
3: double earthWeight = Double.parseDouble(args[0]);
4: double mass = earthWeight/EARTH.surfaceGravity();
5:
6: for (Planet p : Planet.values())
7:
8: System.out.printf("Your weight on %s is %f%n",
9:
10: p, p.surfaceWeight(mass));
11:
12: }

извежда резултат

Your weight on MERCURY is 66.107583
Your weight on VENUS is 158.374842
Your weight on EARTH is 175.000000
...
и т.н.

Всъщност реализацията на enum отива толкова далеч, че чрез нея дори може да се проектира различно поведение при изпълнението на методите в зависимост от константата, спрямо която се извикват.

Трябва да споменм,че заради тази модификация възниква и един неприятен проблем – enum вече е ключова дума от езика Java и ако имате изходен код, който съдържа идентификатори enum ще се наложи преработка. За съжаление enum често се ползваше в практиката за обекти от тип Enumeration (въпреки, че е спорно доколко идентификаторът е подходящ в контекста на самоописващия се код). Въпреки това, на фона на плюсовете от представената възможност, преименуването на променливи (където се налага) е съвсем приемлива цена.

Статични импорти Нека е дефиниран един примерен интерфейс, изглеждащ така:

1: package buttons;
2:
3: interface MenuButton {
4:
5: public static final int BTN_OK = 1;
6:
7: public static final int BTN_CANCEL = 2;
8:
9: ...
10:
11: }

При това положение, достъпването на дефинираните по-горе константи става по следния начин:

1: public class ExampleClass implements buttons.MenuButton {
2:
3: boolean isOK(long buttonMask) {
4:
5: return (buttonMask & BTN_OK) != 0;
6:
7: }
8:
9: }

Основния проблем в слчуая е нуждата от имплементация на интерфейса MenuButton, за да може да се осъществява достъп до неговите константи. Това определно не е добра идея и подобно решение води след себе си не малко проблеми (например, ако един клас имплементира подобен интерфейс, всички негови подкласове ще бъдат със “замърсено” от интерфейса пространство за имена. Още повече, ако в някоя от бъдещите си версии, един такъв клас престане да се нуждае от константите, той ще трябва да продължи да имплементира интерфейса, за да гарантира двоична съвместимост). Тази ситуация е толкова позната, че дори се описва чрез специален антиеталаон “Интерфейси за константи”.

За справянето с този род трудности, съгласно новата спецификация се въвеждат стаични импорти, чрез които показания пример се преобразува така:

1: import static buttons.MenuButton;
2:
3: public class ExampleClass {
4:
5: boolean isOK(long buttonMask) {
6:
7: return (buttonMask & BTN_OK) != 0;
8:
9: }
10:
11: }

Аналогично, чрез статично импортиране на System, стандартното System.out.println() може да се замени само с out.println(). От статичните импорти можем да се възползваме и при съкращаване извикванията на методи. Например

import static java.lang.Math;

заменя извикването на Math.sin() само със sin(), което е по-прегледно.

Освен че са полезни, статичните импорти водят до “пристрастяване” и карат някои хора да импортират всеки възможен метод или променлива/константа. Това също не е добре, понеже такъв подход води до по-малка прозрачност на кода (не е много ясно кой метод/променлива от къде идва). Простото правило е, че статични импорти трябва да се ползват основно за избягване на “Интерфейсите за константи” и спестяване на предекларирането на константи локално.

Метаданни (анотации) Много от ползваните в практиката API-та свързани с Java платформата се нуждаят от допълнителни парчета стандартен код или външни файлове, за да бъдат използвани коректно. Например, JavaBean-овете изискват поддръжката на BeanInfo клас, в паралел със самия Bean. По същия начин, Enterprise JavaBean-овете се нуждаят от deployment descriptor-и. В тях също така се използват различни механизми за маркиране на един метод като remote или home. Една добра идея, която помага в подобни случаи, а и елиминира доста грешки е поддръжката на някакъв механизъм за анотации (или метаданни, казано по друг начин).

Java винаги е поддържала анотации в една или друга форма. Модификаторът transient сам по себе си, не е нищо повече от анотация за сериализиращата подсистема. Друг пример е добре познатия @deprecated.

Тези “заложени” в неявна форма възможности в Тигър са значително разширени. В новата версия на J2SE е включена изцяло нова система за анотации с общо предназначение. Тя включва синтаксис за деклариране на анотиращи типове, синтаксис за създаване на анотации към декларации, API-та за четене и обработка на анотации, класове представящи анотации и отделен инструмент за обработка (Annotation Processing Tool или apt).

Добре да имаме предвид, че самите анотациите нямат ефект върху поведението на програмта. Те по-скоро променят начина по който програмата се третира от различни инструменти и библиотеки. Те могат да четат и обработват анотации от сорс код, компилиран клас файл и дори по време на работа на самта програма.

Анотациите се дефинират като интерфейси. Най-простия вид анотация е т.н. маркер. Тя се различава от останлите по това, че не съдържа никакви елементи. Примерна дефиниция за такава анотация може да бъде:


1: public @interface Unstable {
2:
3: }

Както се вижда, единствената разлика е присъствието на знака @ пред ключовата дума interface. В последствие, можем да маркираме произволен метод ето така:


1: @Unstable void foo() {
2:
3: ...
4:
5: }

В последствие, така анотираните методи могат да бъдат подложени на специална обработка чрез apt или достъпвани по време на изпълнение на приложението. Един интересен пример, който ще разгледаме е код, който тества всички методи маркирани като Unstable в даден клас:


1: public static void main(String[] args) throws Exception {
2:
3: for (Method m : Class.forName(args[0]).getMethods()) {
4:
5: if (m.isAnnotationPresent(Unstable.class)) {
6:
7: try {
8:
9: m.invoke(null);
10:
11: } catch (Throwable ex) {
12: System.out.printf("Test %s failed: %s %n", m, ex.getCause());
13: }
14:
15: }
16:
17: }
18:
19: }

Освен вече споменатата анотация тип “маркер”, съществуват още два тип анотации – анотации с единична стойност и пълни анотации.

Анотациите с единична стойност са близки до маркерите, но в допълнение могат да съхраняват и някаква данна (под формата на една променлива). Тъй като е известно, че те пазят една и само една променлива, тяхното използване прилича по-скоро на извикване на метод (например @SingleValueAnotationExample(“variable data”)). Една примерна дефиниция на такава анотация може да изглежда така:


1: public @interface TODO {
2:
3: String value();
4:
5: }

Така показанта анотация лесно може да разшири към пълна:


1: public @interface TODO {
2:
3: int id();
4:
5: String value();
6:
7: String author() default “Unknown”;
8:
9: }

В този случай използването на TODO ще изглежда така:


1:@TODO(
2: id="1",
3: value ="This metod is to be extended with variable arguments",
4: author ="Nikolay Manchev "
5: ) public void foo(String arg) {
6:
7: ...
8:
9: }

Горния пример илюстрира и възможността за задаване на стойности по подразбиране за различните полета на анотацията (директива default).

Системата за анотации на Tiger е сложна. Освен показаната базова идеология, тя включва и множество готови анотации като @SupressWarnnings,@Documented, @Inherited и т.н. Възможно е дори да се постави анотация към анотация. Повече информация може да се намери в спецификация JSR 175 (описваща въвеждането на метаданни в Java), но дори и на пръв поглед се вижда, че анотациите са доста мощен инструмент с широко приложние.

Variable Arguments Друго съществено нововъведение е възможността да бъдат предадени множество аргументи на даден метод, без техния брой да е предварително известен. За да се възползва от това нововъедение, един метод трябва да промени декларацията на аргументите си по следния начин:

1: void foo ( Object... args) {
2: ...
3: }

Съответно, извикването на метода може да бъде

foo ( “аргумент_1”, “аргумент_2”, “аргумент_3”);

Това, което системата прави невидимо е да пакетира така предадените аргументи и да ги предаде като масив. Тази техника и до сега се използваше от повечето разработчици, когато им трябваше променлив брой аргументи. Именно поради факта, че реализацията е направена по този начин (пакетиране в масиви) системата ще приеме за валиден аргумент не само подаване на поредица стойности, но и директно предаване на масив съдържащ аргументите. Този факт прави тази възможност идеално съвместима със вече съществуващите API-та.

Използването на променлив брой аргументи е полезно в много случай (вж. метод printf() във Форматиран вход/изход). Въпреки това, създаването на такива методи трябва да е изключително умерено и тази възможност е добре да се ползва само там където наистина има реална полза. Важно е да обърнем внимание на факта, че ако например предефинираме в класа един такъв метод, за ползвателите ще е трудно да открият кой точно вариант на метода се извиква.

Форматиран вход/изход Една дългоочаквана промяна, която повечето разработчици приветстват е включването на printf() метод, който се държи аналогично на добре познатия printf от C. Интересното при него е, че той се възползва от новата възможност за предаване на нерегламентирай брой аргументи, чрез която в случая се предават променливите за отпечатване. Всъщност самата дефиниция на printf изглежда така

public PrintStream printf(String format, Object... args)

А следните примери илюстрира и използването на новия метод:

System.out.printf("e = %+10.4f", Math.E)

// -> "e = +2,7183"

System.out.format("Local time: %tT", Calendar.getInstance());

// -> "Local time: 13:34:18"

System.err.printf("Unable to open file '%1$s': %2$s", fileName, exception.getMessage());

// -> "Unable to open file 'food': No such file or directory"

и т.н.

Използвания запис за форматиращата част е познатият от клас java.util.Formatter и може да бъде намерен подробно описан в API-то на J2SE 1.5.

Новата спецификация внася допълнителни модификации и при четенето на данни. До момента, изчитането на една целочислена стойност от клавиатурата изглеждаше така:


1: BufferedReader br = new BufferedReader(new InputStreamReader(System.in));    
2:
3: try {
4:
5: String str = br.readLine();
6:
7: int n = Integer.parseInt(str);
8:
9: } catch (IOException ioex) {
10:
11: // process exception if necessery
12:
13: }

В новата версия, може направо да се използва класа Scanner (в пакета java.util) и същия ефект да се постигне с доста по-чиста конструкция:


1: Scanner reader = new Scanner(System.in);
2: int n = reader.nextInt();


В заключение В тази статия накратко споменахме нововъденията направени в езика Java, които се появяват за първи път в J2SE 5.0. Това е една много малка част от измененията и новите възможности на платформата. Можем да споменем също така подобренията по виртуалната машина (ускоряване на стартирането на приложенията чрез Class Data Sharing, възможността за настройване на “цели” за виртуалната машина и адапитране към тях, подобрения по Garbage Collector-а, възможността за автоматично откриване дали приложението се стартира върху сървърна машина, промяна в управлението на нишките, нова система за обработка на грешките, нов, изключително прецизен механизъм за отичтане на време – System.nanoTime() и т.н. и т.н.)

Сериозни промени са направени и в базовите библиотеки (Lang и Util пакетите), мрежовата част, сигурността и интернационализацията. Подобрена е поддръжката на променливите от обкръжението, рефлексията подържа новите параметризирани типове. Разширени са и интеграционните библиотеки – RMI (SSL/TLS Socekt Factory), JDBC (RowSet е имплементиран в JdbcRowSet, CachedRowSet, FilteredRowSet и др.). Редица нововъведения са направени и в частта за CORBA, Java IDL, Java RMI-IIOP (подробен списък с новите възможности е публикуван на http://java.sun.com/j2se/1.5.0/docs/guide/idl/jidlChanges.html ).

От гледна точка на визуалния интерфейс, AWT вече използва Unicode API-та върху 2000/XP и могат да избегнат зависимостта от локалните настройки на операционната система. Десетки са допълненията към Java Sound Technology и Java2D API-то. Интересни допълнения има и в Swing (изключая новите Look & Feel-ове, JTable вече има поддръжка за директен печат и най-важното : най-накрая jFrame.add() може да се използва вместо jFrame.getContentPane().add())

Редно е да споменем също така промените по различните инстурменти и архитектурата : Включен е нов интерфейс JVMTI (Java Virtual Machine Tool Interface), чрез който може да се наблюдава състоянието и да се контролира процеса на изпълнение за произволни приложения, работещи върху виртуалната машина. Освен това са направени редица нововъведения в JPDA (Java Platform Debugger Architecture). Javadoc 5.0 също представя редица разширения. Сървърната J2SE 5.0 вече официално поддържа и AMD Opteron.

Нека спрем до тук, но да не забравяме - Sun все още не са казали последната си дума. От компанията вече обявиха, че разработката на J2SE 6.0 (с кодово име Mustang) се движи по график и ще бъде представена през пролетта на 2006.

Николай Манчев, 2004
PC Magazine, бр. 12, 2004 г., ISSN 1310-3229

Коментари

Име
URL
Код   
Запис
 

КНИГАТА

Oracle Database Security Book
(c) 2004-2008 Николай Манчев. Освен ако изрично не е споменато нещо друго, всички материали публикувани тук се разпространяват под Creative Commons Attribution License. Материали, коментари и изображения, които не са създадени и подписани от мен са собственост на съответните им автори.