Rozdział 2. Interakcja ze środowiskiem

Niniejszy rozdział opisuje, w jaki sposób programy tworzone w języku Java mogą się komunikować ze środowiskiem wykonawczym. W pewnym sensie wszystkie czynności, jakie program utworzony w Javie wykonuje, korzystając z Java API, mają związek ze środowiskiem. W tym rozdziale skoncentruję się jednak na elementach środowiska bezpośrednio związanych z programem. Jednocześnie przedstawię także klasę System, dającą dostęp do informacji o używanym przez nas systemie.

Warto także przedstawić dwie klasy wykonawcze. Pierwsza z nich — java.lang.Runtime — w niewidoczny sposób zapewnia możliwości funkcjonalne wielu metod klasy System. Na przykład działanie metody System.exit() sprowadza się do wywołania metody Runtime.exit(). Z technicznego punktu widzenia jest to „część środowiska”, niemniej jednak bezpośrednio jest ona wykorzystywana wyłącznie w przypadkach uruchamiania innych programów, co opisałem w „24.1. Uruchamianie zewnętrznego programu”.

Siódma wersja systemu Unix, wprowadzona w 1979 roku, dysponowała fascynującą nową możliwością określaną jako zmienne środowiskowe. Zmienne środowiskowe są dostępne we wszystkich nowoczesnych wersjach systemu Unix (w tym także w systemie Mac OS X) oraz w większości nowszych systemów obsługiwanych z poziomu wiersza poleceń, takich jak DOS lub wiersz poleceń w systemie Windows; nie znajdziemy ich jednak w niektórych starszych platformach systemowych oraz w niektórych środowiskach uruchomieniowych Javy. Zmienne te nie są również dostępne w komputerach Macintosh, Palm Pilot, SmartCard oraz w innych środowiskach języka Java. Zazwyczaj zmienne środowiskowe są powszechnie stosowane do dostosowywania środowiska wykonawczego na danym komputerze, stąd też pochodzi ich nazwa. Przykładem takiej zmiennej, doskonale znanym większości Czytelników, może być zmienna systemowa PATH, która w systemach Unix oraz DOS określa, gdzie system ma szukać programów. A zatem pojawia się oczywiste pytanie — w jaki sposób można pobierać wartości zmiennych środowiskowych w programach napisanych w języku Java?

Otóż można to zrobić we wszystkich nowych wersjach Javy, niemniej uzależniając działanie programu od możliwości określania zmiennych środowiskowych i korzystania z nich, należy zachować dużą ostrożność, gdyż w niektórych rzadkich przypadkach systemy operacyjne mogą ich nie udostępniać. Jest jednak raczej mało prawdopodobne, byśmy znaleźli się w takiej sytuacji, gdyż aktualnie wszystkie „standardowe” systemy operacyjne dla komputerów osobistych obsługują zmienne środowiskowe.

W niektórych, bardzo starych wersjach Javy metoda System.getenv() była odrzucana lub po prostu nie działała. Aktualnie metoda getenv() nie jest już odrzucana, choć jej zastosowanie wciąż powoduje wyświetlenie ostrzeżenia, że zamiast niej lepiej skorzystać z właściwości systemowych (patrz „2.2. Pobieranie informacji z właściwości systemowych”). Nawet w systemach operacyjnych, które ją obsługują, nazwy zmiennych środowiskowych są traktowane w różny sposób — niektóre rozróżniają w nich wielkość liter, a inne nie. Przykład 2-1 przedstawia krótki program, który korzysta z metody getenv().

Wykonanie tego programu spowoduje wyświetlenie następujących wyników:

C:\javasrc>java environ.GetEnv.java
System.getenv("PATH") = C:\windows\bin;c:\jdk1.8\bin;c:\users\ian\bin
C:\javasrc>

Bezargumentowa wersja metody System.getenv() zwraca wszystkie dostępne zmienne środowiskowe, przekazując je jako mapę (obiekt Map) niezmiennych łańcuchów znaków. Mapę tę można przejrzeć i uzyskać dostęp do wszystkich ustawień użytkownika bądź pobrać kilka ustawień systemowych.

Obie wersje metody getenv() wymagają posiadania uprawnień do dostępu do środowiska, więc zazwyczaj nie działają w środowiskach o ograniczonych możliwościach, takich jak aplety.

Czym w zasadzie jest właściwość? Właściwość to para nazwa-wartość przechowywana w obiekcie java.util.Properties (przedstawię go bardziej szczegółowo w „7.12. Zapisywanie łańcuchów znaków w obiektach Properties i Preferences”).

Obiekt System.Properties kontroluje i opisuje środowisko wykonawcze Javy. Klasa System posiada statyczną właściwość Properties, której zawartość stanowi zbiór informacji o systemie operacyjnym (na przykład os.name), danych dostosowujących system (java.class.path) oraz właściwościach definiowanych w wierszu poleceń (którymi zajmiemy się w dalszej części receptury). Warto zwrócić uwagę, iż wykorzystanie kropek w nazwach właściwości (na przykład: os.arch, os.version, java.class.path, java.lang.version) sprawia wrażenie, że pomiędzy właściwościami występują relacje analogiczne do związków pomiędzy nazwami klas. Niemniej klasa Properties nie wymusza żadnych takich relacji — każdy klucz jest po prostu łańcuchem znaków, a kropki nie mają żadnego szczególnego znaczenia.

Do pobierania wartości właściwości systemowych służy metoda System.getProperty(). Jeśli należy pobrać wszystkie właściwości, można się posłużyć metodą System.getProperties(). Aby zatem sprawdzić, czy została zdefiniowana właściwość o nazwie „pensil color”, można użyć następującej instrukcji:

String color = System.getProperty("pencil color");

Jednak jaką wartość zwraca wywołanie metody System.getProperty()? Java zapewne nie jest na tyle „inteligentnym” językiem, aby znać ulubiony kolor kredki wszystkich jej użytkowników. Racja, nie jest! Jednak w bardzo prosty sposób można zdefiniować ulubiony kolor kredki (lub jakąkolwiek inną informację), tak aby Java mogła z tej informacji skorzystać. Do tego celu wykorzystywana jest opcja -D.

Opcja -D służy do definiowania wartości przechowywanych w obiekcie właściwości systemowych. Za nią należy podać nazwę, znak równości oraz wartość. Ten łańcuch znaków zostanie przetworzony w taki sam sposób jak zawartość pliku właściwości (patrz „7.12. Zapisywanie łańcuchów znaków w obiektach Properties i Preferences”). W jednym wywołaniu, przed nazwą klasy, można umieścić kilka opcji -D. W systemach Unix lub Windows wywołanie może przyjąć następującą przykładową postać:

java -D"pencil color=Deep Sea Green" environ.SysPropDemo

W przypadku korzystania ze zintegrowanego środowiska programistycznego parę nazwa-wartość definiującą właściwość systemową należy podać w odpowiednim oknie dialogowym, przy czym zazwyczaj będzie to okno Run Configuration.

Program SysPropDemo może pobrać wartość jednej lub kilku właściwości systemowych, a zatem można go uruchomić także w następujący sposób:

$ java environ.SysPropDemo os.arch
os.arch = x86

Przy okazji warto również wspomnieć o kodze zależnym od używanego systemu operacyjnego. „2.3. Określanie używanej wersji JDK” jest poświęcona kodowi zależnemu od konkretnej wersji JDK, natomiast „2.4. Tworzenie kodu zależnego od używanego systemu operacyjnego” — kodowi zależnemu od używanego systemu operacyjnego.

Szczegółowe informacje na temat wykorzystania oraz sposobów określania nazw plików właściwości podam w „7.12. Zapisywanie łańcuchów znaków w obiektach Properties i Preferences”. Strona WWW z dokumentacją klasy java.util.Properties dokładnie opisuje reguły używane przez metodę load(), jak również zawiera wszelkie inne informacje dotyczące plików właściwości.

Choć Java ma gwarantować przenośność zarówno kodu, jak i skompilowanych programów, to jednak istnieją poważne różnice pomiędzy poszczególnymi wersjami środowisk wykonawczych Javy. Czasami może się okazać, że konieczne będzie stworzenie rozwiązania zastępującego pewną możliwość dostępną w nowych wersjach środowiska wykonawczego, której nie ma w środowiskach starszych. W takich przypadkach pierwszą informacją, którą należy zdobyć, jest numer wersji JDK odpowiadający używanemu środowisku wykonawczemu Javy. Informację tę można uzyskać przy wykorzystaniu metody System.getProperty():

System.out.println(System.getProperty("java.specification.version"));

Można się także zdecydować na nieco bardziej ogólne rozwiązanie i sprawdzać istnienie bądź brak konkretnych klas. Jednym ze sposobów, by to zrobić, jest skorzystanie z metody Class.forName("klasa") (więcej informacji na jej temat można znaleźć w Rozdział 23.), która zgłasza wyjątek, jeśli klasy nie uda się wczytać — to bardzo wyraźny sygnał, że podana klasa nie jest dostępna w bibliotece środowiska wykonawczego. Poniżej przedstawiłem kod takiego rozwiązania, stanowiący fragment aplikacji, która chce sprawdzić, czy są dostępne wspólne komponenty Swing UI (zazwyczaj są one dostępne we wszystkich nowoczesnych standardowych implementacjach Java SE; nie są natomiast dostępne w muzealnej wersji JDK 1.1 ani w wersji Javy stosowanej w systemie Android). W dokumentacji języka można znaleźć informację, w której wersji JDK ta klasa się pojawiła — została ona podana w sekcji pod nagłówkiem Since. Jeśli takiej sekcji nie ma w dokumentacji, to zazwyczaj oznacza to, że dana klasa jest dostępna od początku istnienia języka:

starting/CheckForSwing.java

public class CheckForSwing {
    public static void main(String[] args) {
        try {
            Class.forName("javax.swing.JButton");
        } catch (ClassNotFoundException e) {
            String failure =
                "Przepraszam, ale ta wersja aplikacji wymaga \n" +
                "Javy z nowszymi komponentami JFC/Swing,\n" +
                "należącymi do pakietu javax.swing.*";
            // Lepiej zadbać, by coś się pojawiło na ekranie. Może
            // to być JOptionPane albo myPanel.add(new Label(failure));
            System.err.println(failure);
        }
        // Tu nic nie trzeba wyświetlać - GUI powinno działać...
    }
}

Niezwykle ważne jest rozróżnienie pomiędzy sprawdzaniem dostępności klasy podczas kompilacji oraz w trakcie wykonywania programu. W obu przypadkach należy skompilować powyższy fragment kodu w komputerze, w którym zainstalowana wersja języka Java zawiera poszukiwane klasy — w podanych przykładach są to odpowiednio JDK 1.1 oraz Swing. Testy tego typu są jedynie próbą pomocy dla osób korzystających ze starszych wersji Javy, które chciałyby używać nowoczesnych aplikacji napisanych w tym języku. Ich celem jest prezentacja komunikatów, które byłyby nieco bardziej opisowe niż standardowy komunikat class not found („nie znaleziono klasy”) wyświetlany przez środowisko wykonawcze Javy. Koniecznie należy zaznaczyć, że program nie będzie w stanie wykonać powyższych testów, w przypadku gdy zostaną one umieszczone wewnątrz kodu zależnego od klas, których dostępność sprawdzamy. Jeśli w środowisku JDK 1.0 kod sprawdzający dostępność biblioteki Swing zostanie umieszczony w konstruktorze klasy potomnej klasy JPanel, to jego wyniki nigdy nie będą wyświetlane (proszę to przemyśleć). Testy tego typu należy umieszczać możliwie jak najwcześniej w głównej części programu, zanim zostaną utworzone jakiekolwiek obiekty związane z graficznym interfejsem użytkownika. W przeciwnym przypadku kod realizujący testy będzie niepotrzebnie zajmować miejsce w nowszych środowiskach Javy i nigdy nie będzie wykonywany w środowisku Java 1.0. Oczywiście przedstawiony przykład sprawdzał dostępność możliwości, która pojawiła się w bardzo wczesnej wersji języka Java, jednak tej samej techniki można użyć do sprawdzania dowolnej możliwości środowiska wykonawczego, dodanej na którymkolwiek z etapów rozwoju języka (informacje dotyczące możliwości dodawanych w poszczególnych wersjach języka można znaleźć w Dodatek B). Techniki tej można używać także do sprawdzania, czy jakieś inne, dodatkowe biblioteki zostały pomyślnie dodane do ścieżki klas.

Omówienie czynności realizowanych przez metody klasy Class przedstawię w Rozdział 23.

Choć Java jest językiem przenośnym, niektóre jej elementy są zależne od używanego systemu operacyjnego. Dotyczy to na przykład separatorów stosowanych w nazwach plików. Każdy użytkownik systemów Unix wie, że funkcję separatora pełni znak ukośnika (/), a znak odwrotnego ukośnika (\) służy do podawania znaków specjalnych. Pod koniec lat 70. zespół pracowników firmy Microsoft pracował nad systemem Unix — ich wersja nosiła nazwę Xenix, a prace nad nią zostały następnie przejęte przez SCO. Zespół zajmujący się tworzeniem systemu DOS poznał i polubił model systemu plików wykorzystywany w Uniksie. System MS-DOS 2.0 nie posiadał kartotek, lecz jedynie „numery użytkowników”, podobnie jak system, na jakim był wzorowany — Digital Research CP/M (który z kolei bazował na różnych innych systemach operacyjnych). A zatem pracownicy firmy Microsoft zdecydowali się skopiować organizację systemu plików Uniksa. Niestety już wcześniej wykorzystali znak ukośnika do oddzielania opcji; w systemach Unix służy do tego znak minusa (-); także znak separatora ścieżek (podawanych w zmiennej systemowej PATH) został wcześniej wykorzystany jako „separator litery napędu”, na przykład C: lub A:. Z powyższych powodów polecenia mają aktualnie postać wskazaną w Tabela 2-1.

Ale do czego mogą się nam przydać te informacje? Otóż jeśli należy wygenerować ścieżkę dostępu do pliku w programie napisanym w języku Java, to trzeba wiedzieć, czy poszczególne jej elementy należy oddzielać znakiem /, \, czy też jeszcze jakimś innym. W języku Java problem ten można rozwiązać na dwa sposoby. Po pierwsze, przenosząc kod z systemów Unix do Windows (i na odwrót); dopuszczalne jest stosowanie bądź ukośnika, bądź też odwrotnego ukośnika[13], a kod komunikujący się z systemem operacyjnym odpowiednio obsłuży te znaki. Po drugie, Java udostępnia informacje zależne od systemu operacyjnego w formie niezależnej od używanego systemu. Otóż jeśli chodzi o separator nazw plików (oraz separator używany przy określaniu wartości zmiennej systemowej PATH), klasa java.io.File (omówiona bardziej szczegółowo w Rozdział 11.) udostępnia kilka zmiennych statycznych zawierających te informacje. Ponieważ klasa File jest zależna od używanego systemu operacyjnego, przechowywanie w niej tych informacji jest sensownym rozwiązaniem. Zmienne te zostały przedstawione w Tabela 2-2.

Separatory nazwy plików oraz ścieżki dostępu są znakami, lecz dla wygody można je także pobrać w formie łańcucha znaków.

Drugim, bardziej ogólnym mechanizmem jest systemowy obiekt Properties, o którym wspominałem już w „2.2. Pobieranie informacji z właściwości systemowych”. Można go wykorzystać do określenia używanego systemu operacyjnego. Poniżej przedstawiłem kod programu wyświetlającego listę cech używanego systemu operacyjnego. Bardzo pouczające może być wykonanie tego programu w kilku różnych systemach.

environ/SysPropDemo.java

public class SysPropDemo {
    public static void main(String[] argv) throws IOException {
        if (argv.length == 0)
            System.getProperties().list(System.out);
        else {
            for (String s : argv) {
                System.out.println(s + " = " +
                    System.getProperty(s));
            }
        }
    }
}

Na przykład niektóre systemy operacyjne dysponują mechanizmem określanym jako „pseudourządzenie” (ang. null device), którego można używać w celu ignorowania wszelkich danych wyjściowych generowanych przez program. (Mechanizm ten jest zazwyczaj wykorzystywany do celów szacowania efektywności programów). Poniżej przedstawiłem przykład, który odczytuje wartość właściwości "os.name" i na tej podstawie określa nazwę pseudourządzenia (jeśli w danym systemie operacyjnym pseudourządzenie nie jest dostępne, zwracany jest łańcuch znaków tmpplk, oznaczający, że w danym systemie operacyjnym będziemy od czasu do czasu tworzyć pliki pomocnicze, które będą usuwane, gdy tylko przestaną być potrzebne):

com/darwinsys/lang/SysDep.java

package com.darwinsys.lang;

import java.io.File;

/** Niektóre rozwiązania zależne od systemu operacyjnego.
 * Wszystkie metody są statyczne.
 * @author Ian Darwin
 */
public class SysDep {

    final static String UNIX_NULL_DEV = "/dev/null";
    final static String WINDOWS_NULL_DEV = "NUL:";
    final static String FAKE_NULL_DEV = "tmpplk";

    /** Zwraca nazwę pseudourządzenia w systemach, które je obsługują,
     *  lub łańcuch znaków "tmpplk", jeśli używany system operacyjny
     *  nie udostępnia pseudourządzeń.
     */

    public static String getDevNull() {

        if (new File(UNIX_NULL_DEV).exists()) {     ➊
            return UNIX_NULL_DEV;
        }

        String sys = System.getProperty("os.name"); ➋
        if (sys==null) {                            ➌
            return FAKE_NULL_DEV;
        }
        if (sys.startsWith("Windows")) {            ➍
            return WINDOWS_NULL_DEV;
        }
        return FAKE_NULL_DEV;                       ➎
    }
}
  • ➊ Jeśli /dev/null istnieje, to go używamy.

  • ➋ Jeśli nie, to sprawdzamy, czy we właściwościach systemowych jest zapisana nazwa systemu operacyjnego.

  • ➌ Nie ma, zatem dajemy za wygraną i zwracamy tmpplk.

  • ➍ Wiemy, że używany jest system Windows, więc zwracamy NUL:.

  • ➎ Jeśli żaden z warunków nie został spełniony, to zwracamy tmpplk.

Istnieje jeden przypadek, w którym konieczne jest sprawdzanie, czy używanym systemem operacyjnym jest Mac OS. Otóż Mac OS X udostępnia grupę komponentów graficznego interfejsu użytkownika, które są dostępne tylko w tym systemie i których należy używać, by tworzone aplikacje bardziej przypominały „natywne” aplikacje pisane z myślą o tym systemie. Zagadnienie to zostało bardziej szczegółowo przedstawione w „14.18. Korzystanie z rozszerzonych możliwości pakietu Swing w systemie Mac OS X”. Najprościej rzecz ujmując, Apple zaleca, by sprawdzać dostępność łańcucha mrj.version i na tej podstawie określać, czy aplikacja działa w systemie Mac OS X:

boolean isMacOS = System.getProperty("mjr.version") != null;

Kiedy tworzone aplikacje będą coraz bardziej złożone, w coraz większym stopniu będziemy także musieli korzystać z dodatkowych bibliotek tworzonych przez innych programistów i firmy. Należy je dodać do zmiennej CLASSPATH.

Niegdyś zalecane było umieszczanie plików JAR w specjalnym katalogu mechanizmu rozszerzającego języka Java, który zazwyczaj nosi nazwę \jdk1.2\ jre\lib\ext, a nie wymienianie każdego z nich w zmiennej CLASSPATH. Jednak obecnie rozwiązanie takie nie jest ogólnie zalecane.

Zaletą stosowania zmiennej CLASSPATH w porównaniu z mechanizmem rozszerzającym jest to, że w ten sposób wyraźniej widać, od czego nasza aplikacja zależy. Programy takie jak Ant (patrz „1.6. Automatyzacja kompilacji przy użyciu programu Ant”) lub Maven (patrz „1.7. Automatyzacja zależności, kompilacji, testowania i wdrażania przy użyciu programu Apache Maven”) oraz wszelkie zintegrowane środowiska programistyczne mogą znacznie ułatwić albo nawet całkowicie zautomatyzować dodawanie plików JAR do zmiennej CLASSPATH.

Kolejną wadą stosowania katalogu mechanizmu rozszerzającego jest to, że wymaga on modyfikowania zainstalowanego JDK i środowiska wykonawczego, co może powodować problemy z utrzymaniem systemu oraz w przypadku instalacji nowszych wersji JDK lub JRE.

Oczekuje się, że w wersji Java 9 zostanie wprowadzony nowy mechanizm pozwalający na modularyzację aplikacji, więc najprawdopodobniej nie będziemy chcieli inwestować zbyt wiele wysiłku w zbyt skomplikowane rozwiązania. Należy zatem korzystać z istniejących narzędzi, które opisałem powyżej.

Programiści pracujący w systemie Unix musieli się borykać z tym problemem znacznie dłużej niż inni, dlatego też opracowali funkcję o nazwie getopt[14] (napisaną w języku C). Funkcja ta przetwarza argumenty podane w wierszu wywołania programu i poszukuje wśród nich jednoliterowych opcji poprzedzonych znakiem - oraz argumentów opcjonalnych. Na przykład poniższe polecenie:

sort -n -o outfile myfile1 myfile2

wywołuje standardowy program sort dostępny w systemach Unix, Linux i Mac OS. Opcja -n informuje program, że poszczególne rekordy zawierają raczej wartości numeryczne niż cyfrowe, a opcja -o nakazuje zapisanie wyników w pliku o nazwie outfile. Pozostałe elementy wiersza wywołania programu — słowa myfile1 oraz myfile2 — są traktowane jako pliki wejściowe, których zawartość należy posortować. W systemach Windows argumenty wywołania programu są poprzedzane znakiem ukośnika (/). W naszej funkcji posłużymy się formą stosowaną w systemie Unix — znakiem minusa. Oczywiście można zmodyfikować przykład tak, aby wykorzystywane były znaki ukośnika.

Każda instancja GetOpt jest tworzona z myślą o rozpoznawaniu konkretnego zestawu argumentów, gdyż każdy program zazwyczaj ma ściśle określony, stały zestaw argumentów, które można podawać podczas jego uruchamiania. Możemy zatem utworzyć tablicę obiektów GetOptDesc reprezentujących dozwolone argumenty. W przypadku programu przedstawionego powyżej moglibyśmy użyć następującego kodu:

environ/GetOptParseArgsDemo.java

GetOptDesc[] options = {
    new GetOptDesc('n', "numeric", false),
    new GetOptDesc('o', "output-file", true),
};
Map<String, String> optionsFound = new GetOpt(options).parseArguments(args);
if (optionsFound.get("n") != null) {
    System.out.println("sortType = NUMERIC;");
}
String outputFile = null;
if ((outputFile = optionsFound.get("o")) != null) {
    System.out.println("Plik wyjściowy to: " + outputFile);
} else {
    System.out.println("Wyniki zapisywane w System.out");
}

Najprostszym sposobem korzystania z klasy GetOpt jest wywołanie metody parseArguments.

Z myślą o osobach, które nauczyły się korzystać z funkcji getopt() dostępnej w języku C w systemach Unix, także nasza klasa GetOpt udostępnia metodę getopt(), której można używać w pętli while. Każde jej wywołanie zwraca bądź to jedną odnalezioną prawidłową opcję, zapisaną jako znak, bądź też stałą DONE, kiedy wszystkie opcje zostały już zwrócone (zakładając, że w ogóle jakieś były).

Przedstawiony poniżej program używa klasy GetOpt do sprawdzenia, czy w wierszu wywołania programu została podana opcja -h (wywołująca system pomocy):

environ/GetOptSimple.java

public class GetOptSimple {
    public static void main(String[] args) {
        GetOpt go = new GetOpt("h");
        char c;
        while ((c = go.getopt(args)) != 0) {
            switch(c) {
            case 'h':
                helpAndExit(0);
                break;
            default:
                System.err.println("Nieznana opcja w " +
                    args[go.getOptInd()-1]);
                helpAndExit(1);
            }
        }
        System.out.println();
    }

  /** Miejsce na wyświetlenie pomocy.
   * Oczywiście można wyświetlać znacznie obszerniejsze informacje
   * niż te przedstawione poniżej.
   */
    static void helpAndExit(int returnValue) {
        System.err.println("Tutaj można umieścić informacje, ");
        System.err.println("jak korzystać z tego programu.");
        System.exit(returnValue);
    }
}

Przedstawiony poniżej dłuższy program rozpoznaje kilka różnych opcji:

environ/GetOptDemoNew.java

public class GetOptDemoNew {
    public static void main(String[] argv) {
        boolean numeric_option = false;
        boolean errs = false;
        String outputFileName = null;

        GetOptDesc[] options = {
            new GetOptDesc('n', "numeric", false),
            new GetOptDesc('o', "output-file", true),
        };
        GetOpt parser = new GetOpt(options);
        Map<String,String> optionsFound = parser.parseArguments(argv);
        for (String key : optionsFound.keySet()) {
            char c = key.charAt(0);
            switch (c) {
                case 'n':
                    numeric_option = true;
                    break;
                case 'o':
                    outputFileName = (String)optionsFound.get(key);
                    break;
                case '?':
                    errs = true;
                    break;
                default:
                    throw new IllegalStateException(
                    "Nieznany znak opcji: " + c);
            }
        }
        if (errs) {
            System.err.println("Użycie: GetOptDemoNew [-n][-o plik][plik...]");
        }
        System.out.print("Opcje: ");
        System.out.print("Numeryczna: " + numeric_option + ' ');
        System.out.print("Plik wyjściowy: " + outputFileName + "; ");
        System.out.print("Pliki wejściowe: ");
        for (String fileName : parser.getFilenameList()) {
            System.out.print(fileName);
            System.out.print(' ');
        }
        System.out.println();
    }
}

Poniżej przedstawiłem wyniki wywołania tego programu w przypadku użycia różnych opcji:

C:\javasrc> java environ.GetOptDemoNew
Opcje: Numeryczna: false Plik wyjściowy: null; Pliki wejściowe:

C:\javasrc> java environ.GetOptDemoNew -M
Użycie: GetOptDemo [-n][-o plik][plik...]
Opcje: Numeryczna: false Plik wyjściowy: null; Pliki wejściowe:

C:\javasrc> java environ.GetOptDemoNew -n a b c
Opcje: Numeryczna: true Plik wyjściowy: null; Pliki wejściowe: a b c

C:\javasrc> java environ.GetOptDemoNew -numeric a b c
Opcje: Numeryczna: true Plik wyjściowy: null; Pliki wejściowe: a b c

C:\javasrc> java environ.GetOptDemoNew -numeric -o /tmp/foo a b c
Opcje: Numeryczna: true Plik wyjściowy: /tmp/foo; Pliki wejściowe: a b c

Dłuższy przykład, prezentujący wszystkie możliwości tej wersji klasy GetOpt, można znaleźć w dostępnym w internecie repozytorium darwinsys-api w katalogu src/main/test/lang. Z kolei jej kod źródłowy jest umieszczony w tym samym repozytorium w pliku src/main/java/com/darwinsys/lang/GetOpt.java, przedstawiłem go także na Przykład 2-2.

Przykład 2-2. Kod źródłowy klasy GetOpt

com/darwinsys/lang/GetOpt.java

// package com.darwinsys.lang;
public class GetOpt {
    /** Lista plików podanych po argumentach. */
    protected List<String> fileNameArguments;
    /** Zestaw poszukiwanych znaków. */
    protected final GetOptDesc[] options;
    /** Położenie w opcjach. */
    protected int optind = 0;
    /** Publiczna zmienna ustalona reprezentująca "brak
     * dalszych opcji" */
    public static final int DONE = 0;
    /** Flaga wewnętrzna - czy zostały przetworzone wszystkie opcje. */
    protected boolean done = false;
    /** Bieżący argument opcji. */
    protected String optarg;

    /** Pobiera argument bieżącej opcji; zapis typowy dla
     * systemów UNIX. */
    public String optarg() {
        return optarg;
    }
    /** Pobiera argument bieżącej opcji; zapis typowy dla Javy. */
    public String optArg() {
        return optarg;
    }

    /** Tworzy obiekt GetOpt na podstawie specyfikacji opcji podanych
     * w tablicy obiektów GetOptDesc. To preferowany konstruktor.
     */
    public GetOpt(final GetOptDesc[] opt) {
        this.options = opt.clone();
    }

    /** Tworzy obiekt GetOpt, zapisując w nim zestaw znaków opcji.
     * To stary konstruktor, zapewniający zgodność wstecz.
     * Jednak łatwiej go stosować, jeśli nie musimy używać opcji o długich
     * nazwach, dlatego też nie zamierzam go "odrzucać".
     */
    public GetOpt(final String patt) {
        if (patt == null) {
            throw new IllegalArgumentException("Wzorzec nie może być pusty.");
        }
        if (patt.charAt(0) == ':') {
            throw new IllegalArgumentException(
                "Niewłaściwy wzorzec, nie może się zaczynać od znaku ':'");
        }

        // Pierwszy przebieg: zliczamy wszystkie litery opcji we wzorcu.
        int n = 0;
        for (char ch : patt.toCharArray()) {
            if (ch != ':')
                ++n;
        }
        if (n == 0) {
            throw new IllegalArgumentException(
                "Nie znaleziono żadnych opcji w łańcuchu " + patt);
        }

        // Drugi przebieg: tworzymy tablicę obiektów GetOptDesc.
        options = new GetOptDesc[n];
        for (int i = 0, ix = 0; i<patt.length(); i++) {
            final char c = patt.charAt(i);
            boolean argTakesValue = false;
            if (i < patt.length() - 1 && patt.charAt(i+1) == ':') {
                argTakesValue = true;
                ++i;
            }
            Debug.println("getopt",
                "KONSTRUKTOR: opcje[" + ix + "] = " + c + ", " + argTakesValue);
            options[ix++] = new GetOptDesc(c, null, argTakesValue);
        }
    }

    /** Przywrócenie wartości początkowych. */
    public void rewind() {
        fileNameArguments = null;
        done = false;
        optind = 0;
        optarg = null;
    }

    /**
     * Nowoczesny sposób stosowania klasy GetOpt: wywołujemy ją raz
     * i pobieramy wszystkie opcje.
     * <p>
     * Ta metoda analizuje opcje i zwraca obiekt Map, którego kluczami
     * są znalezione opcje.
     * Zazwyczaj po niej należy wywołać metodę getFilenameList().
     * <br>Efekt uboczny: w polu "fileNameArguments" zapisuje nową listę.
     * @return Obiekt Map, którego kluczami są łańcuchy składające się
     *    z jednego znaku (zawierają one dopasowane opcje), a wartościami
     *    są łańcuchy znaków zawierające wartości opcji lub wartość null,
     *    jeśli dana opcja była bezargumentowa.
     */
    public Map<String, String> parseArguments(String[] argv) {
        Map<String, String> optionsValueMap = new HashMap<String, String>();
        fileNameArguments = new ArrayList<String>();
        for (int i = 0; i < argv.length; i++) {    // Nie możemy użyć pętli
                                        // foreach, potrzebujemy zmiennej i
            Debug.println("getopt", "parseArg: i=" + i + ": arg " + argv[i]);
            char c = getopt(argv);    // ustawia globalne pole "optarg"
            if (c == DONE) {
                fileNameArguments.add(argv[i]);
            } else {
                optionsValueMap.put(Character.toString(c), optarg);
                // Jeśli to argument, to przygotowujemy się, by go pominąć.
                if (optarg != null) {
                    i++;
                }
            }
        }
        return optionsValueMap;
    }

    /** Pobiera listę argumentów w postaci przypominającej nazwy plików,
     * umieszczoną za opcjami; używana wyłącznie po wcześniejszym
     * wywołaniu metody parseArguments.
     */
    public List<String> getFilenameList() {
        if (fileNameArguments == null) {
            throw new IllegalArgumentException(
                "Nie można wywoływać metody getFilenameList() przed wywołaniem parseOptions()");
        }
        return fileNameArguments;
    }

    /** To najważniejsza metoda tej klasy, niezależnie od tego, jak jej
     * używamy. Zwraca jeden argument; jest wywoływana w pętli, aż zwróci
     * DONE.
     * Efekty uboczne: zmienia pola globalne: optarg i optind.
     */
    public char getopt(String argv[]) {
        Debug.println("getopt",
            "optind=" + optind + ", argv.length="+argv.length);

        if (optind >= (argv.length) || !argv[optind].startsWith("-")) {
            done = true;
        }

        // Jeśli skończyliśmy (teraz lub wcześniej), to spadamy stąd.
        // Nie należy łączyć tej instrukcji z poprzednią.
        if (done) {
            return DONE;
        }

        optarg = null;

        // XXX TODO - drugi przebieg, pierwszy sprawdza długie nazwy opcji,
        // a drugi obsługuje zastosowania zaawansowane, takie jak
        // możliwość zapisu opcji "-n -o plikWyjściowy" w formie
        // "-no plikWyjściowy".

        // Pobieramy następny argument wiersza wywołania; jeśli zaczyna
        // się od "-", to szukamy go na liście prawidłowych opcji.
        String thisArg = argv[optind];

        if (thisArg.startsWith("-")) {
            for (GetOptDesc option : options) {
                if ((thisArg.length() == 2 &&
                        option.getArgLetter() == thisArg.charAt(1)) ||
                   (option.getArgName() != null &&
                    option.getArgName().equals(thisArg.substring(1)))) { // znaleziony
                    // Jeśli potrzebuje argumentu, to go pobieramy.
                    if (option.takesArgument()) {
                        if (optind < argv.length-1) {
                            optarg = argv[++optind];
                        } else {
                            throw new IllegalArgumentException(
                                "Opcja " + option.getArgLetter() +
                                " wymaga wartości, a znaleziono koniec listy opcji.");
                        }
                    }
                    ++optind;
                    return option.getArgLetter();
                }
            }
            // Zaczyna się od "-", lecz nie jest prawidłową opcją,
            // a zatem uznajemy to za błąd.
            ++optind;
            return '?';
        } else {
            // Znaleziono słowo, które nie jest ani opcją, ani argumentem,
            // traktujemy je jako koniec listy opcji.
            ++optind;
            done = true;
            return DONE;
        }



    }

    /** Zwraca wartość pola optind, indeks ostatniej sprawdzanej opcji
     * w tablicy args. */
    public int getOptInd() {
        return optind;
    }

}


[13] Tworząc łańcuchy znaków w programach przeznaczonych do użycia w systemie Windows, należy pamiętać o stosowaniu podwójnych znaków odwrotnego ukośnika, gdyż w większości innych miejsc oprócz wiersza poleceń systemu MS-DOS znak \ ma specjalne znacznie; na przykład String rootDir = "C:\\";.

[14] W systemach Unix dostępnych jest kilka różnych wersji funkcji getopt. Przedstawiona tu wersja dosyć dokładnie odpowiada standardowej funkcji opracowanej przez AT&T, usprawniając ją o kilka udogodnień, takich jak obsługa argumentów o długich nazwach.