Tablice, wejście – wyjście, wyjątki, formatowanie
Tablice, przechowywanie obiektów
Do przechowywania wielu wartości (zmiennych prostych) tego samego typu służą tablice.
Tablice mogą służyć do przechowywania obiektów (referencji) tego samego typu.
Tablice są najbardziej wydajnym sposobem składowania obiektów. Można tworzyć tablice o rozmiarze znanym podczas liczenia (tablice dynamiczne). Tablice mają wady. Jedną z nich jest to, że nie można ich powiększać w trakcie obliczeń. Oznacza to, że po zbudowaniu odpowiadającego naszym wyobrażeniom o tablicach nie da się później, w sposób wydajny, zmienić rozmiaru tablicy.
Wyjście poza zakres tablicy jest sygnalizowane (wyrzucanie wyjątku), co istotnie ułatwia życie programistom.
Wiele algorytmów wymaga tworzenia obiektów, których liczba jest znana dopiero podczas wykonania programu i w trakcie wykonania programu zmieniana, w sposób nieznany, podczas pisania programu. Zdarza się również, że nie wiadomo, nie tylko ile obiektów zostanie utworzonych, ale również ich typ nie jest do końca sprecyzowany. Potrzebna jest więc możliwość tworzenia dowolnej liczby obiektów, w dowolnej chwili i miejscu. Jest to bardzo istotny problem programistyczny
Oprócz tablic, w Javie jest kilka innych możliwości przechowywania obiektów. Są to różnorodne kolekcje. Liczba przechowywanych w kolekcjach obiektów może się zmieniać w trakcie działania programu. W projekcie kolekcji Javy począwszy od wydania piątego nastąpiły istone zmiany. Zdefiniowano wiele nowych metod. Wprowadzono parametryzację kolekcji i metod.
Przejdziemy do omówienia tablic, a pozostałe kolekcje zostawimy na później. Warto pamiętać, już na wstępie, że w tablicach typów podstawowych są przechowywane wartości, a w tablicach obiektów są przechowywane referencje (inaczej odwołania) do obiektów. Element tablicy obiektów jest odwołaniem do obiektu, a nie samym obiektem.
Przyjrzyjmy się i prześledźmy działanie programu, który zilustruje budowanie i wykorzystywanie tablic zmiennych prostych (wartości), tablic obiektów (referencji). Będą to tablice jedno i wielo -indeksowe.
P10Tablice.java
Tablica jest ponumerowanym ciągiem zmiennych typów podstawowych lub obiektów. Tablice zmiennych podstawowych definiuje się za pomocą operatora indeksującego [] - int[] a; lub int a[];. Są to równoważne zapisy. Nie można podać rozmiaru tablicy. Zapis postaci int aa[20]; jest błędny.
Definicja tablicy a nie rezerwuje pamięci, jest jedynie referencją do tablicy. Próba odwołania się do obiektu a skończy się błędem. (np: System.out.print("a " + a.length);). Sama deklaracja jest poprawna, można w dalszym ciągu np: napisać a = new int[5]; Teraz a nie jest już pustą referencją.
Rezerwacja miejsca w pamięci operacyjnej dla pewnej liczby elementów musi być zrobiona za pomocą operatora new i winna mieć postać: int[] b = new int[5]; W ten sposób mamy zainicjowanych 5 elementów typu int z wartościami domyślnymi równymi zeru. Tablicą b można się posługiwać (dokładniej jej elementami) tak samo jak w języku C.
Tablica b jest wyprowadzana na standardowe wyjście za pomocą zwykłej instrukcji for i instrukcji for-each.
Podobnie jak dla typów podstawowych, definicja identyfikatora c buduje pustą referencję.
Tablica d jest tablicą pustych referencji zainicjowanych na wartość null. Jest to tablica obiektów. Każdy obiekt musi być zbudowany przez odwołanie do odpowiedniego konstruktora. Następna pętla inicjuje elementy tablicy referencjami do obiektów klasy Liczby. Zainicjowane są wszystkie elementy z wyjątkiem ostaniego. Ilustruje to następny fragment programu.
Tablica d jest wyprowadzana na standardowe wyjście za pomocą zwykłej instrukcji for, i instrukcji for-each.
Tablice e, f ilustrują inne metody definiowania tablic obiektów. Metoda toString() z klasy Liczby pozwala na prosty zapis wyprowadzania równoważników tekstowych tej klasy. Spójrz na wydruki pchodzące z tego programu.
Operacje wejścia wyjścia, wyjątki, formatowanie, ustawienia lokalne
Umiejętność wprowadzania danych ze standardowego wejścia i z pliku oraz zapisywanie wyników do pliku (standardowe wyjście już znamy) należą do podstawowych umiejętności.
Podczas wykonywania operacji WE/WY mogą pojawić się błędy - wyjątki, musimy uwzględnić w programie ten fakt dołączając listę obsługiwanych wyjątków.
Dane i wyniki powinny mieć określony formacie. Format danych i wyników jest zależny od ustawień lokalnych.
Nazwa pliku z danymi i nazwa pliku (ścieżki dostępu) z wynikami mogą być argumentami wywołania programu.
Wszystkie te zadania są zrealizowane poniższym programie.
P11Wewy.java
Argumenty wywołania programu
W argumentach metody main znajduje się tablica łańcuchów tekstowych. W argumentach aktualnych, argumentach wywołania programu, podajemy nazwy dwóch plików: plik z danymi i plik z wynikami (args[0], args[1]).
W Netbeans, nazwy argumentów programu należy ustawić w "Properties->Run->Arguments" (menu kontekstowe w widoku projektu, dla projektu) projektu.
Metody wyrzucające wyjątki
W nagłówku metody main jest napis throws IOException. Dlaczego taka informacja musi się znaleźć? Wystarczy wiedzieć, że jedna z metod wykorzystywanych w programie może wyrzucać wyjątek. W dokumentacji należy sprawdzić każdą stosowaną w programie metodę pod kątem obsługi wyjątków. Przyjrzyjmy się opisowi metody readLine() w dokumentacji. Ma ona postać następującą:
readLine public String readLine() throws IOException Read a line of text. A line is considered to be terminated by any one of a line feed ('\n'), a carriage return ('\r'), or a carriage return followed immediately by a linefeed. Returns: A String containing the contents of the line, not including any line-termination characters, or null if the end of the stream has been reached Throws: IOException - If an I/O error occurs |
---|
I to powoduje, że do metody main należy dołączyć informację o możliwości wyrzucania wyjątków klasy IOException. Więcej na temat wyjątków i sposobów ich przechwytywania i obsługi dowiemy się w następnych wykładach. Bez informacji o możliwości wyrzuczania wyjątków zostanie zgłoszony błąd kompilacji. Nie wszystkie wyjątki muszą być w ten sposób opisywane.
Buforowane wejście
Obiekt System.in odpowiada standardowemu urządzeniu wejścia. Jest budowany automatycznie. Obiekt klasy InputStreamReader odpowiada znakowemu strumieniowi wejściowemu. Argumentem jego konstruktora jest standardowe wejście. Obiekt odpowiadający buforowanemu wejściu (z domyślną długością bufora), jest klasy BufferedReader. Ostatecznie otrzymujemy.
BufferedReader in1 = new BufferedReader(new InputStreamReader(System.in));
Metoda readLine(), pozwala na przeczytanie ze standardowego wejścia ciągu znaków, aż do napotkania nowej linii; znak nowej linii nie jest częścią tekstu. Zwraca ona referencję do obiekut klasy String.
Dla sprawdzenia, drukowany jest wprowadzony uprzednio tekst oraz jego długość (metoda length()) na obiekcie klasy String. Jeśli przeczytany tekst reprezentuje liczbę całkowitą, to można go zamienić na wartość typu całkowitego (metody valueOf(), intValue() z klasy String) i wykorzystać do zdefiniowania tablicy o podanym podczas wykonania programu rozmiarze. W następnych liniach wykonywane są proste operacje na tablicy zmiennych całkowitych.
Czytanie danych prościej
W wersji 5 Javy powstała klasa Scanner, która upraszcza czytanie danych ze standardowego wejścia. W przykładzie wykorzystana jest metoda nextInt()
Czytanie danych z pliku
Tworzony jest obiekt klasy BufferedReader o nazwie in2, który podobnie jak uprzednio obiekt in1, pozwala na buforowane czytanie, ale tym razem z pliku. Jako argument do konstruktora BufferedReader przekazujemy obiekt klasy FileReader zamiast InputStreamReader.
Z pliku o nazwie dane czytane są dwie linie tekstu. Pierwsza linia tekstu służy do zbudowania obiektu klasy Float przez wykonanie statycznej metody valueOf() z klasy Float, która zamieni przeczytany tekst na liczbę typu float i zwróci referencję do obiektu klasy Float. Druga zainicjuje zmienną podstawową przez zmienienie przeczytanego tekstu na liczbę typu float za pomocą statycznej metody parseFloat() z klasy Float (funkcja jest wywoływana na rzecz klasy).
Czytanie danych z pliku, ustawienia lokalne
Klasa Scanner może być wykorzystana do czytania danych z pliku. W przykładzie jest to zapisane w komentarzu. Z pliku "wyniki" czytane są wartości typu float. Jeśli w systemie ustawiono język polski, to używany jest przecinek dziesiętny, nie kropka. Chcąc przeczytać dane napisane w pliku "dane", musimy zmienić ustawienia lokalne (statyczne metody getDefault(), setDefault() z klasy Locale umożliwiające pobranie i wstawienie ustawień lokalnych).
Formatowanie wyjścia
Do formatowania wyjścia może posłużyć klasa Formatter i jej metody format. Metoda ta jako pierwszy argument przyjmuje tekst określający sposób wyprowadzenia danych. Formatownie danych jest bardzo podobne do znanego formatowania w języku C. Następne argumenty to zmienne, których wartości będą wyprowadzane.
Metoda format() przygotowuje tekst do wyprowadzenia w zmiennej klasy StringBuilder w obiekcie klasyFormatter. Każde następne wykonanie metody format(), na rzecz tego samego obiektu, dołączy nowy tekst do wcześniej zapisanego. We wszystkich wyprowadzanych danych są uwzględnione ustawienia lokalne.
Metoda format() może być wykonana na rzecz obiektu klasy PrintWriter i System.out. W każdym wywołaniu są uwzględnione ustawienia lokalne.
Pisanie danych do pliku
Pisanie do pliku jest analogiczne do czytania z pliku. Należy utworzyć obiekt klasy PrintWriter. Jest to obiekt o nazwie out1. Jest on tworzony za pomocą konstruktora z argumentem klasy BufferedWriter() z argumentem z klasy FileWriter tworzonym za pomocą konstuktora z argumentem klasy String. Czyli kolejno:
FileWriter pom1 = new FileWriter("wyniki"); BufferedWriter pom2 = new BufferedWriter(pom1); PrintWriter out1 = new PrintWriter(pom2); |
---|
Zamykanie otwartych plików
Każdy otwarty plik powinien być zamknięty. Brak zamknięcia pliku może pozbawić, wykonującego program, części lub całości wyprowadzanych wyników.
Tablice - klasa Arrays i pożyteczne metody tej klasy
Następny przykład pokaże kilka pożytecznych metod klasy Arrays. Prześledzimy poniższy przykład.
P13Tablice.java
Zastosowanie metod klasy Arrays
Podstawowe operacje na tablicach takie jak: zapełnianie tablic, sortowanie, porównywanie są metoami klasy Arrays - warto się z nimi zapoznać. Są to w większości metody statyczne, a więc nie wymagają budowania obiektów klasy Arrays. W tym przykładzie poznamy kilka z nich, pozostałych jak zwykle należy szukać w dokumentacji. Przeanalizujmy przykład.
Tablica a1 jest tablicą trójelementową o elementach typu int zainicjowaną w znany sposób. Wszystkie jej elementy mają wartość 25. Tablica a2 jest tablicą trójelementową o elementach typ int zainicjowaną na wartości zerowe. Następnie tablica a2 jest wypełniana wartościami 25 za pomocą metody fill() zastosowanej do klasy Arrays. Wartości zapisane w tej tablicy są wyprowadzane na standardowe wyjście. Tablice a1 i a2 są porównywane za pomocą metody equals() z klasy Arrays otrzymujemy wartość true, ponieważ porównywane są odpowiadające sobie elementy dwóch tablic. Następna instrukcja porównuje te dwie tablice za pomocą metody metody equals() odziedziczonej z klasy Object. W tym przypadku porównywane są referencje tablic a1 i a2, są to różne referencje i w wyniku porównania otrzymujemy wartość false.
Tablice a3 i a4 są tablicami obiektów klasy Integer wypełnionymi wartościami 25 za pośrednictwem odpowiednio konstruktora i metody fill() z klasy Arrays. I znowu metoda equals() odziedziczona z klasy Object i metoda equals()z klasy Arrays dają różne rezultaty. Tak więc metoda equals() z klasy Arrays w zależności od przekazanego argumentu wykonuje różne algorytmy.
Tablica a5 jest trójelementową tablicą zawierającą teksty ("ala"), tablica a6 jest również trójelementową tablicą zawierającą tekst "ala" - wypełnioną za pomocą metody fill() z klasy Arrays. Podobnie jak uprzednio porównanie tych tablic za pomocą metody odziedziczoej z klasy Object i metody z klasy Arrays da różne rezultaty. Widać więc, że w klasie Arrays zdefiniowana jest jeszcze jedna metoda o nazwie equals z argumentem, który jest tablicą łańcuchów tekstowych.
Tablica a7 jest jedenastoelementową tablicą zmiennych typu podstawowego. Po zastosowaniu do niej metody sort() z klasy Arrays otrzymujemy tablicę uporządkowaną rosnąco, wyniki działania tej metody są wyprowadzone na monitor.
Tablica a8 jest ośmioelementową tablicą zawierającą teksty. Po zastosowaniu do niej metody sort() z klasy Arrays otrzymujemy tablicę uporządkowaną rosnąco z zachowaniem porządku alfabetycznego. Metoda sort() z klasy Arrays może być zastosowana do tablicy zawierającej teksty.
Klasa Para zawiera dwa prywatne pola: jedno typu int, drugie klasy String.Klasa Para zawiera jeden konstruktor pozwalający na zainicjowanie obydwu pól.
Klasa Para zawiera dwie metody: pierwsza o nazwie dajString() zwracająca pole tekstowe; druga o nazwie toString() tworząca równoważnik tekstowy klasy Para.
Zadaniem naszym jest utworzenie tablicy obiektów klasy Para i wykorzystanie metody Arrays.sort() do posortowania tej tablicy względem pola typu int, a następnie względem pola klasy String. Jest to możliwe dzięki zbudowaniu dwóch klas implementujących klasę Comparator. Są to klasy: PorownajInt i PorownajString.
Comparator to interfejs, można to na razie rozumieć jako klasę będącą wzorcem dla klas, które z tego wzorca mają korzystać. Zadaniem programisty jest opracowanie szczegółów, zgodnych z potrzebami i wzorcem. Zbudowanie klasy wymaganej przez wzorzec pozwoli na wykorzystanie metod i klas z biblioteki. W tym przypadku należy zbudować klasę zawierającą metody porównujące dwa obiekty klasy Para. Opis interfejsu znajduje się w dokumentacji, w spisie klas interfejsy są pisane pochyłym drukiem.
Tekst implements Comparator napisany w nagłówku tych klas nakazuje zaimplementowć metodę compare zgodnie z naszą definicją określającą, jak stwierdzić, który z dwóch obiektów jest większy, mniejszy, czy też równy.
W obydwu klasach implementujących Comparator zdefiniowane są metody o nazwie compare. W klasie PorownajInt zwracane jest -1 lub 0 lub +1 w zależności od wartości pola całkowitego klasy Para. W klasie PorownajString zwracane jest -1 lub 0 lub +1 w zależności od wartości pola zawierającego tekst Para.
W obydwu metodadach compare przyjęto dwa argumenty formalne klasy Object. Wymaga tego odpowiednia metoda compare() z klasy Comparator. W treści metod zostało zastosowane rzutowanie obiektu klasy Object na obiekt klasy Para. W drugiej z metod zastosowano metodę compareTo z klasy String.
Po zdefiniowaniu tych metod, można wykorzystać metodę sort() z klasy Arrays z dwoma argumentami: tablica obiektów i obiekt klasy PorownajInt lub PorownajString. Obiekty te, w algorytmie sortującym będą wykorzystane do porównywania.
Wyniki otrzymane w tych sortowaniach można obejrzeć na terminalu.
Metody z klasy PorownajInt są również zastosowane do dwóch obiektów klasy Para (aa, bb). Najpierw należy utworzyć obiekt klasy PorownajInt (xxx), a następnie na rzecz tego obiektu wykonać metodę PorownajInt przekazując do niej referencje aa i bb. Zgodnie z oczekiwaniem dostaniemy wynik -1, ponieważ 2 jest mniejsze od trzech.
Tablice dwuindeksowe, ciąg dalszy klasy Arrays
Prześledźmy poniższy programu. Program ten składa się z czterech klas.
Są to klasy: TablicaDrukuj - wyprowadzanie na standardowe wyjście tablic dwuindeksowych różnych typów, DwieFloat - klasa pomocnicza, P14Tablice - klasa o dostępności publicznej, umożliwiająca testowanie pozostałych klas.
P14Tablice.java
Opis programu
Klasa TablicaDrukuj zawiera cztery statyczne metody drukuj() o różnych argumentach. Są to metody statyczne, więc dostępne nawet wtedy, gdy żaden obiekt klasy TablicaDrukuj nie jest zainicjowany. Metody są rozróżniane na podstawie nazwy i listy argumentów formalnych. Tak więc metody drukuj() z klasy są różnymi metodami. Metody te pozwalają na wyprowadzenie na ekran dwuindeksowych tablic różnych typów. Każda z tych metod pozwala na wyprowadzanie tablic określonej klasy wraz z opisem, który jest drugim argumentem każdej z metod.
Klasa DwieFloat ma dwie składowe typu float. zawiera trzy metody. Metody te pozwalają na operacje arytmetyczne na składowych klasy i nadpisaną metodę toString() pozwalająca na utworzenia tekstowego równoważnika obiektu tej klasy.
Jeśli przyjrzyjmy się wydrukom w każdej z metod klasy TablicaDrukuj, to zauważymy duże podobieństwo w traktowaniu tablic dwuindeksowych. Tablica dwuindeksowa jest jednoargumentową tablicą o elementach, będących tablicami jednoindeksowymi. Zwróć uwagę na wartości x.length oraz x[i].length (pozwala określenie długości wiersza o indeksie i.
Tablica a1 jest dwuindeksową zmiennych typu podstawowego int. Tablica ta zawiera 3 wiersze i 4 kolumny. Wszystkie elementy tej tablicy są zainicjowane na 0 - zgodnie z założeniami języka (patrz wyniki).
Tablice a2 i a22 są tablicami o 3x4 (rozmiar otrzymujemy przez odpowiednie pogrupowanie danych), wszystkie elementy w bydwu tablicach są takie same. Zastosowanie metody Arrays.equals() do całej tablicy daje wynik false. Zastosowanie te samej metody do wierszy tablicy a2 i a22 daje wynik true. Widać, że tablice dwuindeksowe są podobnie zorganizowane jak w C. Po zmianie jednego elementu w wierszu o indeksie 2 cała tablica a22 jest wyprowadzona na ekran. Wyniki porównania wierszy jest zgodny z oczekiwanym - wiersz o indeksie 2 z tablicy a2 jest różny od wiersza o indeksie 2 z tablicy a22.
Tablica a3 ma elementy określone za pomocą metody Arrayys.fill() zastosowanej do wierszy macierzy a3. Jeszcze raz mamy potwierdzenie takiego samego rozumienia tablic dwuindekswych w C i Javie.
Podobnie zachowują się tablice obiektów. Należy pamiętać, że są to tablice referencji i jeżeli nie zostaną zainicjowane, to próba odwołania się do nich - odwołania do pustej referencji zostanie zasygnalizowana przez kompilator. Tablica a4 jest tablicą o 2 wierszach i 3 kolumnach. Elementami tej tablicy są teksty. Jest to tablica obiektów klasy String.
Tablica a5 i a51 są tablicami zbudowanymi dla krókiego odpoczynku. Tablica a5 jest tablicą obiektów klasy DwieFloat o rozmiarze 3x2. Tablica a51 jest tablicą powstałą przez zastosowanie jednej z metod (dodajDwie()) klasy DwieFloat do zbudowania tablicy elementów typu podstawowego float o rozmiarze dokładnie takim samym jak tablica a5.
Zbudowana i wyprowadzona tablica a6 pokazuje, że można budować tablice o różnej liczbie elementów w każdym wierszu.
Klasa ParametrTablicePisz zawiera jedną metodę, metoda ta jest metodą sparametryzowaną. Parametryzacja dotyczy pierwszego argumentu tej metody. Pierwszy argument może być dwuindeksową tablicą dowolnego typu (typ aktualny zastąpi typ Tablica), typu obiektowego (parametryzacja dotyczy wyłącznie typów obiektowych, nie typów prostych).
Metoda pisz z klasy ParametrTablicePisz jest zastosowana do dbiektów klasy String, DwieFloat i Integer. Jest to odzwierciedlone na wydrukach.
Obiekt a7 jest tablicą obiektów o elementach klasy Integer zbudowanych z wykorzystaniem automatycznego otaczania zmiennych prostych. Przed wersją 5 Javy instrukcja inicjująca elementy tablicy byłaby błędna