Hlavní navigace

Novinky v Javě aneb Tygří spáry

7. 11. 2003
Doba čtení: 8 minut

Sdílet

Zřejmě již koncem tohoto roku uvolní společnost Sun Microsystems, Inc. beta verzi Java Platform Standard Edition 1.5 (codename Tiger). Její autoři tvrdí, že jde o největší zásah do jazyka Java od jeho vzniku. Slibují kratší a čitelnější kód, samozřejmě při zachování kompatibility s předchozími verzemi. Na co se můžete těšit, vám odpoví následující článek.

Opravdu nová Java

Až dosud byly do nových verzí, až na výjimky (např. vnitřní a vnořené třídy, později assertions), přidávány balíky, které rozšiřovaly funkčnost dosud nepokrytým směrem, jako například java.nio a java.util.logging ve verzi 1.4, spolu s vylepšováním JVM a překladače. V nové verzi 1.5 však přibudou přímo nové konstrukty (novým klíčovým slovům se autoři až na jedno vyhnuli), jejichž cílem je zejména zpřehlednit kód a přenést psaní rutinních částí kódu z programátora na překladač. Programátor se tak vyvaruje některých zbytečných chyb a některé typové kontroly bude možné oproti dřívější verzi provést již při překladu.

Mezi hlavní novinky nové verze mají patřit:

  • Generické datové typy
  • Rozšířený cyklus for
  • Autoboxing/Unboxing
  • Výčtový typ
  • Static import
  • Proměnný počet parametrů
  • Metadata

Navíc má být v nové verzi přepracován a vylepšen paměťový model.

Následují části, které stručně přiblíží tato nová vylepšení jazyka. Rozhodně se nejedná o vyčerpávající popis – k tomu by bylo třeba desítek stran a není to ani posláním tohoto článku. Pro vysvětlení jsou použity jednoduché části kódu, které (snad) usnadní pochopení nových principů.

Generické datové typy

Tato poměrně značná inovace má zajistit zejména compile-time typovou kontrolu pro prvky kontejneru. Návrh je opravdu rozsáhlý, a proto se zaměříme jen na hlavní myšlenky. Základem je možnost definovat parametrizovaný typ (do určité míry jde o obdobu šablon v C++). Následuje jednoduchý příklad definice rozhraní a třídy, které používají generické typy:

interface Storage<A>{
  A get();
  void put(A element);
}
...
class Test<A> implements Storage<A>{
  private A test;
  public A get(){
    return test;
  }
  public void put(A element){
    test = element;
  }
}

Použití by pak mohlo být následující:

Storage<Integer> s = new Test<Integer>();
s.put(new Integer(7));
...
System.out.println(s.get().intValue());

Je zřejmé, že generické typy naleznou uplatnění zejména pro kontejnery (v souvislosti s tím také bude upraveno API některých tříd v balíku java.util). Pokud není parametrický typ při vytváření instance uveden, použije se java.lang.Object, což zajistí kompatibilitu již existujícího kódu. Jako parametr nebude možné uvést primitivní datový typ, a to z důvodu zachování odděleného používání primitivních a referenčních datových typů v Javě.

Při použití pro kontejnery je tedy hlavní výhodou kontrola typu vkládaných prvků. U prvků získaných z kontejneru naopak odpadá přetypování z typu Object na skutečně uložený typ. Dostatečně ilustrativní by měl být následující příklad, kdy procházíme kolekci obsahující prvky typu String pomocí iterátoru a odstraňujeme objekty specifických vlastností. Nejprve klasická v současnosti používaná konstrukce:

static void removeHello(Collection c){
  for (Iterator i = c.iterator(); i.hasNext();){
    String s = (String) i.next();
    if(s.equals("Hello")) i.remove();
  }
}

Následuje úprava s využitím generických datových typů:

static void removeHello(Collection<String> c){
  for (Iterator<String> i = c.iterator();
       i.hasNext();)
    if (i.next().equals("Hello")) i.remove();
}

Možností je samozřejmě mnohem více. Lze definovat metody, které mají jako návratový typ uveden generický typ, použít jej lze i ve vnitřních třídách apod.

Rozšířený cyklus for

Jde vlastně o nový příkaz, který se dá popsat jako foreach – in. Základní syntaxe je:

for( Type Identifier : Expression )
  Statement

Expression přitom musí být pole nebo musí implementovat nové rozhraní java.lang.Ite­rable. Zároveň se předpokládá změna rozhraní java.util.Collection tak, aby rozšiřovalo nebo implementovalo java.lang.Ite­rable. Základním přínosem tedy je, že při filtrování kontejneru odpadá nutnost volat metody iterátoru. Nejsnadnější bude opět uvést srovnávací příklad. V současnosti provedeme akci pro všechny prvky například takto:

void printHosts(Collection c){
  for (Iterator i = c.iterator(); i.hasNext();){
    URL url = (URL) i.next();
    System.out.println(url.getHost());
  }
}

To lze pomocí rozšířeného cyklu for zapsat například takto:

void printHosts(Collection c){
  for (Object o : c)
    System.out.println(((URL) o).getHost());
}

Při současném použití generických datových typů je pak možné celý zápis upravit dokonce na:

void printHosts(Collection<URL> c){
  for (URL url : c)
    System.out.println(url.getHost());
}

Je vidět, že výsledný kód je přehlednější a zároveň není na programátárovi požadováno, aby psal často se opakující a triviální část kódu.

Autoboxing/unboxing

Ve stejném duchu jako předchozí úpravy je i úprava tato. Protože jazyk Java má jak primitivní typy, tak reference na objekty, jsou pro primitivní typy definovány obalové objektové typy. Dosud však byl převod mezi nimi poměrně komplikovaný.

Při použití autoboxingu/un­boxingu se zápis výrazně zkrátí a zpřehlední. Obalovacímu typu lze přiradit přímo hodnotu typu, který je obalen. Přiřazení je možné provést i naopak. Předpoklad pro finální verzi je, že není možné převést hodnotu null na primitivní typ.

Je zřejmé, že použití této funkce se opět uplatní zejména při práci s kontejnery. Na něco takového jsem čekal již dlouho ;). Uveďme srovnávací příklad. Nyní:

int sumNumbers(List c){
  int sum;
  for (Iterator i = c.iterator(); i.hasNext();){
    Integer number = (Integer) i.next();
    sum += number.intValue();
  }
  return sum;
}

Bystrý čtenář navíc zjistil, že předchozí kód samozřejmě může generovat ClassCastException. Při použití dosud uvedených konstrukcí a autoboxingu/un­boxingu pak kód bude:

void sumNumbers(List<Integer> c){
  int sum;
  for (Integer number : c)
    sum += number;
  return sum;
}

Je vidět, že kód je kratší, přehlednější a kontrola typu proběhne již při překladu.

Výčtový typ

Další poměrně rozsáhlou změnou je zavedení výčtového typu. Jeho specifikace zabírá několik stránek, proto se ve stručnosti pokusím uvést hlavní myšlenky.

Výčtový typ je specifikován tak, že neporušuje zásady objektově orientovaného programování. Jde vlastně o třídu, která exportuje „self-typed“ (samotypované ;)) konstanty a nemá veřejný konstruktor. Výhodou je, že takto definované konstanty lze požít v přepínači switch i v kontejnerech, například jako klíč pro HashMap. Protože jde vlastně o třídu, je ve výčtovém typu možné definovat vlastní atributy a metody (zřejmě využijí jen zkušení programátoři). Výčtové typy budou mít společného předka java.lang.Enum a implementují rozhraní SerializableComparable.

Nejjednodušší (a zřejmě také nejčastější) definice výčtového typu bude například:

public enum Color{ RED, GREEN, BLUE }

Lze ale například také specifikovat jak se budou instance výčtového typu vytvářet:

public enum Compression{
  LOW(0.9), GOOD(0.5), EXCELLENT(0.3);
  private final double compr;
  Compression(double compr){
    this.compr = compr;
  }
  public double value() {
    return compr;
  }
}

Každý výčtový typ má také atribut VALUES, ve kterém je neměnitelný List položek výčtu v pořadí, v jakém byly definovány. Toho využívá následující příklad, který zároveň ukazuje použití přepínače:

for (Compression c : Compression.VALUES){
  switch(c){
    case Compression.LOW:
      System.out.println("Low: " + c.value());
      break;
    case Compression.GOOD:
      System.out.println("Good: " + c.value());
      break;
    case Compression.EXCELLENT:
      System.out.println("Excellent: " + c.value());
      break;
  }
}

Static import

Cílem tohoto zjednodušení má být opět zpřehlednění kódu. Jde vlastně o importování statických členů tříd, díky čemuž již není nutné uvádět u takových členů jméno třídy, ve které jsou obsaženy. Namísto

Math.abs(x);

můžeme tedy použít

import static java.lang.Math.*;
...
abs(x);

Použijeme-li výčtový typ pro definování vlastních konstant, lze je pak použít pomocí statického importu následujícím způsobem:

public enum TVColors{RED, GREEN, BLUE};
...
import static TVColors.*;
...
int color = RED;

Proměnný počet parametrů

Naprostou novinkou je v Javě také možnost definovat metodu s proměnným počtem parametrů. Základní syntaxi i práci s jednotlivými položkami nejlépe vysvětlí následující příklad, který ukazuje definici funkce  printf:

public static void printf(String format, Object... args){
  int i = 0;
  for (char c : format.toCharArray()){
    if (c == '%')
      System.out.print(args[i++]);
    else if (c == '\n')
      System.out.println();
    else
      System.out.print(c);
  }
}


Jako typ proměnného počtu parametrů (v příkladu Object) je možno použít jakýkoliv typ, včetně primitivního. Klauzuli Typ... argument lze pro danou metodu použít samozřejmě jen jednou a na konci seznamu parametrů. Skutečné parametry se pak při volání oddělují (klasicky) čárkou.

Metadata

Metadata je koncept, který by měl opět ulehčit život programátorům. V moderních API je totiž programátor nucen, aby splnil jejich požadavky, napsat určitou část kódu „zbytečně“. Uveďme příklad:

import java.rmi.*;
public interface DoAction extends Remote{
  public Object action() throws RemoteException;
}
public class DoActionImpl
 extends UnicastRemoteObject implements DoAction{
  public Object action() throws RemoteException{
    //implementace
  }
}

Definice rozhraní je v podstatě triviální. Navíc, změní-li se parametry volané metody, bude třeba upravit jak rozhraní, tak implementující třídu. Právě od těchto těžkostí by měla pomoci metadata.

Pomocí metadatového příkazu @Remote by se uvedená část kódu zapsala následujícím způsobem:

import java.rmi.*;
public class DoAction{
  @Remote public Object action(){
    //implementace
  }
}

Použitý vývojový nástroj by pak sám zajistil generování potřebného rozhraní a překlad. Koncept metadat navíc poskytuje kostru pro dodavatele vývojových nástrojů pro definování atributů. Například jsou již definovány metadatové příkazy pro Java Web Services. Sun předpokládá v této oblasti rozsáhlou činnost.

Závěr

Každý, kdo někdy v Javě napsal více než 100 řádků, jistě uvítá (tedy alespoň podle mého názoru) navržené změny. S úsporou napsaného kódu ubyde programátorských chyb a zároveň se zlepší čitelnost programů. Část odpovědnosti se tak přesune na kompilátor, který by měl nové konstrukce efektivně přeložit. Zároveň však zůstává zachována základní vlastnost Javy, tedy kompatibilita.

Všechny uvedené příklady kromě metadat jsem vyzkoušel v „EarlyAccess“ verzi kompilátoru. Stáhnout si jej můžete (po registraci) ze stránek java.sun.com.

Literatura

Návrh nových rozšíření jazyka
jcp.org/aboutJa­va/communitypro­cess/jsr/tiger/

Rozhovor s J. Blochem, softwarovým architektem Sun Microsystems, Inc.
java.sun.com/fe­atures/2003/05/­bloch_qa.html

Cloud23

Adding Generics to the Java Programming Language
www.lcc.uma.es/do­cencia/ETSIIn­f/isd/AddingGe­nericsToJava(sli­des).pdf


Java je registrovaná obchodní známka Sun Microsystems, Inc.