String processing #1
Search, parse and build strings (including Scanner, String-Tokenizer, StringBuilder, String, and Formatter)
To wymaganie dotyczy pracy z łańcuchami znaków. Musimy wykazać się znajomością API poniższych klas:
- String
- Scanner
- StringTokenizer
- StringBuilder
- StringBuffer
- Formatter
W tym artykule opiszę działanie kluczowych metod tych klas. Zwrócę uwagę na możliwe pułapki i zawiłości. Uzupełnieniem artykułu będą przykładowe pytania jakie mogą pojawić się na egzaminie, oraz projekt z kodem do testów i ćwiczeń z opisywanymi klasami.
Na skróty:
String [Opis] [API]
Scanner [Opis] [API]
StringTokenizer [Opis] [API]
StringBuilder, StringBuffer [Opis] [API StringBuilder] [API StringBuffer]
Formatter [Opis] [API]
Repozytorium z przykładami [GitHub]
Klasa String
Metoda indexOf()
int indexOf(int ch)
int indexOf(int ch, int fromIndex)
int indexOf(String str)
int indexOf(String str, int fromIndex)
Metoda indexOf przyjmuje jako pierwszy parametr znak lub ciąg znaków, które będą wyszukiwane w danym stringu. Jeżeli poszukiwany znak zostanie znaleziony, metoda zwraca jego pozycję, w przeciwnym przypadku metoda zwraca wartość (-1).
Jeżeli wyszukujemy ciąg znaków, metoda zwróci pozycję pierwszego znaku z poszukiwanego ciągu.
Drugim, opcjonalnym parametem jest pozycja, od której ciąg znaków ma być przeszukiwany.
Kilka przykładów:
String text = "Ala ma kota, a kot ma Alę, Ala go kocha, a kot ją wcale";
System.out.println(text.indexOf("A")); // zwróci 0
System.out.println(text.indexOf(" ")); // zwróci 3
System.out.println(text.indexOf("kot")); // zwróci 7, zwróć uwagę, że jest to początek słowa 'kota', a my szukamy tylko ciągu 'kot', w tej sytuacji wyrazy nie mają znaczenia.
System.out.println(text.indexOf("Kot")); //Zwróci -1, wielkość liter ma znaczenie
System.out.println(text.indexOf("kot",50));//Zwróci -1, szukana fraza istnieje w przeszukiwanym stringu, ale my rozpoczynamy szukanie od pozycji 50, po 50 znaku nie ma już ciągu 'kot'.
System.out.println(text.indexOf("ma",-100));//Zwróci 4, jak najbardziej możemy podać ujemny index.
System.out.println(text.indexOf("kot",2000));//Zwróci -1, O dziwo, nie zakończy się wyjątkiem IndexOutOfBoundException.
Metoda lastIndexOf()
int lastIndexOf(int ch)
int lastIndexOf(int ch, int fromIndex)
int lastIndexOf(String str)
int lastIndexOf(String str, int fromIndex)
Metoda lastIndexOf działa analogicznie do indexOf, z tą różnicą, że jeśli szukany znak lub ciąg występuje kilka razy w danym stringu, to zostanie zwrócona pozycja ostatniego z nich.
Kilka przykładów:
String text = "Kasia, Asia i Basia i znowu Kasia";
System.out.println(text.lastIndexOf("Kasia"));// Zwróci 28 - początek drugiego wyrazu "Kasia"
System.out.println(text.lastIndexOf("K")); // zwróci 0
System.out.println(text.lastIndexOf("K",10)); // zwróci 0, drugi argument działa tu odwrotnie niż w metodzie indexOf. W tym przykładzie ciąg "K" szukany jest między pozycją 0 a 10.
System.out.println(text.lastIndexOf("K",-2)); //zwróci -1, taki zapis jest dozwolony,teoretycznie oznacza, że mamy szukać "K" nie dalej niż do pozycji -2 ponieważ znaki zaczynają się od pozycji 0, ciąg nie jest przeszukiwany i zwracane jest -1
System.out.println(text.lastIndexOf("Asia",8));//Zwróci 7, zauważ, że ciąg "Asia zaczyna się na 7 pozycji, ale kończy na 10. 10 wykracza poza podany zakres wyszukiwania. Ale metody indexOf i lastIndexOf zwracają zawsze pozycję początkową szukanego ciągu. W tym przypadku jest to 7 i mieści się w podanym zakresie.
Metoda contains()
boolean contains(CharSequence)
Metoda ta zwraca nam wartość true/false w zależności od tego czy string zawiera podany w argumencie ciąg znaków. Zauważ, że argumentem jest tutaj CharSequence, więc z powodzeniem możemy użyć jako argumentu klas: CharBufer, Segment, String, StrigBuffer, StringBuilder. Nie możemy natomiast podać pojedynczego znaku jako (char).
"Test".contains('t'); //Nie skompiluje się!
Kilka przykładów:
System.out.println(text.contains(tutaj));//zwróci true, naturalnie można przekazać do metody wartość z innych obiektów.
System.out.println(text.contains("Pan"));// Zwróci false, wielkość liter ma znaczenie.
Metoda substring()
String substring(int beginIndex)
String substring(int beginIndex, int endIndex)
CharSequence subSequence(int beginIndex, int endIndex)
Metoda substring zwraca ciąg znaków, począwszy od pozycji wskazanej w pierwszym parametrze, do pozycji (endIndex – 1), jeśli podaliśmy parametr endIndex. Jeśli podaliśmy tylko jeden parametr, zwrócony zostanie wycinek od pozycji podanej w parametrze, do końca stringa, z ostatnim znakiem włącznie.
Wiele osób ma problem z określeniem przy różnych operacjach na ciągach znaków, który znak jeszcze wejdzie w zakres substringa, a który już nie. Warto sobie zapamiętać, że:
We wszystkich klasach ze standardowego API Javy (na poziomie egzaminu 1z0-804) praktycznie wszystkie operacje, w których musimy podać jakiś zakres pozycji (min, max), zawsze wezmą pod uwagę znak (obiekt) o indeksie min i zawsze zakończą swoje działanie na indeksie (max -1).
Jeżeli do tego wyrobisz sobie nawyk liczenia wszystkich liczb porządkowych od zera, tak jak to prawie wszędzie robi Java (wyjątkiem jest parsowanie wyniku z zapytania do bazy danych przez JDBC, wtedy pierwsza kolumna ma indeks 1). To już nigdy nie zgubisz się przy kalkulowaniu w głowie substringa.
Metoda subSequence w rzeczywistości woła metodę substring i tak naprawdę została dodana tylko po to, żeby wypełnić kontrakt interfejsu CharSequence, który klasa String implementuje od Javy 1.4. Zapamiętaj tylko, że słowo ‚sequence’ jest w tej metodzie pisane dużą literą w przeciwieństwie do metody substring.
Kilka przykładów:
String text = "0123456789";
System.out.println(text.substring(5));// Zwróci "56789" - od pozycji 5 do końca stringa
System.out.println(text.substring(5,9));// Zwróci "5678" - od pozycji 5 do pozycji (9-1)=8
System.out.println(text.substring(9));// Zwróci "9" - od pozycji 9 do końca stringa
System.out.println(text.substring(9,10));// Zwróci "9" - od pozycji 9 do (10-1)=9, nie będzie IndexOutOfBoundException, mimo, że 10 wykracza poza długość stringa
System.out.println(text.substring(10));// UWAGA, NIE zwróci IndexOutOfBoundException, zwróci pusty ciąg
System.out.println(text.substring(11));// Zwróci IndexOutOfBoundException
Metoda split()
String[] split(String regex)
String[] split(String regex, int limit)
Metoda split dzieli string na tablicę stringów (tokenów). Podział dokonywany jest na podstawie separatora zdefiniowanego w pierwszym argumencie. Jako separator możemy podać znak, ciąg znaków lub wyrażenie regularne. Separator nie zostanie zwrócony w wynikowej tablicy. Drugim, opcjonalnym argumentem metody jest limit zwróconych stringów. Przykładowo, jeżeli podamy limit równy 4, a w przeszukiwanym stringu jest 6 separatorów, to metoda zwróci cztero elementową tablicę, w której pierwsze trzy pozycje będą ciągami znaków wyciętymi zgodnie z separatorem, natomiast na ostatnim miejscu tablicy znajdzie się cała pozostała część pierwotnego stringa, w postaci niezmienionej, czyli łącznie z separatorami.
Jeżeli zdefiniujemy limit większy niż faktyczna ilość tokenów, zwrócona tablica będzie miała rozmiar zgodny ze znalezioną liczbą tokenów, a nie z ustalonym limitem.
Kilka przykładów:
//Użyłem tutaj własnej metody printArray, która wypisuje elementy tablicy oddzielając je przecinkiem. Treść metody jest zawarta w udostępnianym przeze mnie projekcie do ćwiczeń z kodem.
String text = "K O P Y T K O";
printArray(text.split(" ")); // Zwróci tablicę: [K, O, P, Y, T, K, O]
printArray(text.split("T")); // Zwróci tablicę: [K O P Y , K O]
printArray(text.split("\\d"));// Zwróci jedno elementową tablicę [K O P Y T K O], "\\d" to wyrażenie regularne oznaczające cyfry, w naszej tablicy nie ma cyfr, więc string nie zostanie podzielony
printArray(text.split(" ",1)); // Zwróci jedno elementową tablicę [K O P Y T K O], ponieważ ustaliliśmy limit 1 miejsca w zwracanej tablicy, a na ostatnim (a tym przypakdu jedynym) miejscu zwracanej tablicy musi się znaleźć reszta niepodzielonego ciągu
printArray(text.split(" ",0)); // Zwróci tablicę: [K, O, P, Y, T, K, O], limit 0 jest równoznaczny z brakiem limitu
printArray(text.split(" ",-1)); // Zwróci tablicę: [K, O, P, Y, T, K, O], ujemny limit również jest równoznaczny z brakiem limitu
printArray(text.split("[A-Z]"));// Zwróci tablicę: [, , , , , , ] (same spacje), [A-Z] to wyrażenie regularne oznaczające duże litery z zakresu A-Z czyli cały alfabet.
Metoda replace()
String replace(char oldChar, char newChar)
String replace(CharSequence target, CharSequence replacement)
String replaceAll(String regex, String replacement)
String replaceFirst(String regex, String replacement)
Metoda replace przeszukuje stringa pod kątem podanego znaku lub ciągu znaków i zastępuje go ciągiem znaków podanych w drugim argumencie.
Mylące mogą być tutaj nazwy metod replace i replaceAll.
Metoda replace, NIE podmienia tylko jednego wystąpienia poszukiwanego ciągu znaków. Podmienia wszystkie znalezione, tak samo jak metoda replaceAll.
Z tą różnicą, że w metodzie replaceAll, jako pierwszy argument możemy użyć wyrażenia regularnego. Tylko metoda replaceFirst podmienia zawsze jeden znaleziony ciąg znaków i zawsze jest to pierwszy znaleziony ciąg.
Bardzo ważne jest aby pamiętać, że:
Jeżeli dowolna z tych metod nie znajdzie ani jednego ciągu znaków do podmiany to zwrócona zostanie referencja do tego samego obiektu String, na którym wołaliśmy metodę.
Jest to jedna z wyjątkowych sytuacji, w których metoda zwracająca obiekt String, nie zwraca nowo utworzonego obiektu, a przypominam, że String jest tzw. klasą niezmienną (ang. immutable), takiego obiektu po zainicjowaniu nie możemy już zmienić.
Mały przykład, żeby lepiej to zobrazować:
Ile referencji oraz ile obiektów zostanie utworzonych po wywołaniu tego kodu?
String egzamin= „Java ”;
egzamin = egzamin + „SE7”
Powstanie jedna referencja do klasy String, referencja będzie miała nazwę „egzamin”. Oraz powstaną dwa obiekty klasy String. Pierwszy z nich będzie zawierał ciąg „Java „, a drugi „Java SE7”. Wraz z przypisaniem drugiego obiektu do referencji „egzamin”, obiekt o treści „Java ” pozostanie bez żadnych referencji w systemie i zostanie przeznaczony do usunięcia przez Garbage Collector. Na egzaminie mogą pojawić się podobne pytania.
Kilka przykładów:
String text = "Dwa kopytka i pierog";
//System.out.println(text.replace('i', "oraz"));// Nie skompiluje się, nie można zamienić char na string, tylko char na char
System.out.println(text.replace('i', '&')); // Zwróci "Dwa kopytka & pierog"
System.out.println(text.replace(" ", "-"));// Zwróci "Dwa-kopytka-i-pierog" - podmieniane są wszystkie znaki pasujące do podanego argumentu, a nie tylko jeden.
System.out.println(text.replace("[a-z]", "1")); //Zwróci niezmieniony string "Dwa kopytka i pierog". W pierwszym argumencie podaliśmy wyrażenie regularne, ale metoda replace nie próbuje interpretować pierwszego argumentu jako wyrażenie.
System.out.println(text.replaceAll("[a-z]", "1")); //Zwróci "D11 1111111 1 1111ó1", zgodnie z podanym wyrażeniem małe litery zostały zamienione na "1".
System.out.println(text.replaceFirst(" ", "--")); //Zwróci "Dwa--kopytka i pierog" - tylko pierwsza spacja została podmieniona
//System.out.println(text.replaceFirst('D', 'd')); // Nie skompiluje się, metoda replaceFirst przyjmuje tylko typ String jako argumenty.
Klasa Scanner
Klasy Scanner możemy użyć do podzielenia ciągu znaków na fragmenty (tokeny) analogicznie jak za pomocą metody split z klasy String. Z tą różnicą, że Scanner może operować nie tylko na stringach, ale również na strumieniach (w tym plikach).
Konstruktory klasy Scanner:
Scanner(File source)
Scanner(File source, String charsetName)
Scanner(InputStream source)
Scanner(InputStream source, String charsetName)
Scanner(Path source)
Scanner(Path source, String charsetName)
Scanner(Readable source)
Scanner(ReadableByteChannel source)
Scanner(ReadableByteChannel source, String charsetName)
Scanner(String source)
Jak widzisz, Scanner możemy utworzyć dla obiektów o takim typie jak File, InputStream, Path czy zwykły String. Dodatkowo, drugim, opcjonalnym argumentem możemy określić stronę kodową naszego źródła.
Zapamiętaj, że
w żadnym z konstruktorów scannera nie podajemy jaki separator ma zostać użyty do rozdzielnia ciągów znaków!
Domyślnie separatorem takim są wszelkie „whitespace” czyli spacje i znaki specjalne \t \r \n.
Możesz ustawić własny separator korzystając z metody useDelimiter.
Zwróć uwagę- useDelimiter, a nie setDelimiter.
Scanner useDelimiter(Pattern pattern)
Scanner useDelimiter(String pattern)
Aby z obiektu typu Scanner pobrać kolejny dostępny token używamy metody next(), która zwraca następny token jako String.
Możemy też odpytać o token konkretnego typu:
BigDecimal nextBigDecimal() BigInteger nextBigInteger() boolean nextBoolean() byte nextByte() double nextDouble() float nextFloat() int nextInt() String nextLine() long nextLong() short nextShort()
Zanim jednak odpytamy Scanner o obiekt konkretnego typu, musimy być pewni, że następny w kolejności token będzie się dało rzutować na nasz pożądany typ. Przykładowo metoda nextInt() NIE będzie szukała wśród tokenów pierwszego możliwego inta, tylko weźmie pierwszy token z brzegu i spróbuje go rzutować na typ int. Jeśli się to nie uda, zostanie zwrócony InputMismatchException.
Dlatego każda z metod next() ma swoją odpowiednią metodę: hasNext(), hasNextInt(), hasNextBoolean() itd… Wskazują one czy token, który ma zostać zwrócony jako następny da się rzutować na odpowiedni typ.
Kilka przykładów:
Scanner s;
s = new Scanner("Abra 1 1d Kadabra 2 ala 3 kazam!"); //Domyślnym dleimiterem jest spacja.
System.out.println(s.hasNextInt());// Zwróci false, następny (pierwszy) token jest Stringiem;
s.next(); //Pobieramy pierwszy token i nic z nim nie robimy;
System.out.println(s.next()); //Mimo, że kolejnym tokenem będzie "1", który da się rzutować na int, bez problemu możemy pobrać go jako string
System.out.println(s.hasNextInt()); // zwróci false, następnym tokenem jest "1d". Nie da sie go rzutować na int.
s = new Scanner("1 R 2 A 3 D 4 E 5 K");
s.useDelimiter("\\d\\s"); //delimiterem jest dwuznakowy ciąg "dowolna cyfra plus biały znak (np spacja)"
System.out.println(s.hasNextInt()); // Zwróci false, mimo, że pierwszym znakiem jest '1', wszystkie cyfry interpretujemy jako delimitery, więc żadna z nich nie będzie tokenem
System.out.println(s.nextInt()); //Jeśli jednak spróbujemy pobrać inta, zwrócony zostanie wyjątek InputMismatchException.
Klasa StringTokenizer
Klasa StringTokenizer ma podobną funkcjonalność jak Scanner. Jednak można z jej pomocą podzielić tylko obiekt String. Obiekt ten musimy podać już w konstruktorze.
Dodatkowo w konstruktorze powinniśmy zadeklarować delimiter, ponieważ nie ma możliwości zmienienia go na stałe za pomocą metody, tak jak w klasie Scanner. Choć możliwe jest odpytanie o kolejny token z uwzględnieniem separatora innego niż podany w konstruktorze.
Oto wszystkie możliwe konstruktory StringTokenizer:
StringTokenizer(String str) StringTokenizer(String str, String delim) StringTokenizer(String str, String delim, boolean returnDelims)
Uwaga, jeśli w konstruktorze przekażemy null jako delimiter, obiekt zostanie utworzony bez żadnych błędów, ale próba odpytania o token zakończy się wyrzuceniem NullPointerException.
Klasa ta ma również stosunkowo mało metod, więc wypiszę je wszystkie:
- int countTokens() – zwraca liczbę znalezionych tokenów
- boolean hasMoreElements() , boolean hasMoreTokens() – obie te metody sprawdzają czy są jeszcze jakieś tokeny do zwrócenia.
- Object nextElement() – zwraca następny token jako obiekt typu Object.
- String nextToken() – zwraca następny token jako obiekt typu String.
- String nextToken(String delim) – zwraca następny token, ale posługuje się delimiterem podanym w argumencie, a nie w konstruktorze.
Pomimo, że StringTokenizer jest w wymaganiach do egzaminu 1Z0-804, to jest to klasa, której użytkowania się już nie zaleca. Jego funkcjonalność w większości pokrywa metoda split() klasy String.
Kilka przykładów:
StringTokenizer st1 = new StringTokenizer("Raz1dwa trzy 7cztery"); //Domyślnym delimiterem są białe znaki
System.out.println(st1.countTokens()); //zwróci 3
System.out.println(st1.nextToken("1"));// Zwróci "Raz";
System.out.println(st1.nextToken("1"));// Zwróci "dwa trzy 7cztery", tyko jeśli poprzednia linijka też się wykonała.
System.out.println(st1.nextToken(" "));// Jeśli poprzednie 2 linijki sie wykonały, to zwróci NoSuchElementException, ponieważ posługując się poprzednim delimiterem doszliśmy już do ostatniego tokenu, nie możemy dzielić od nowa z innym delimiterem.
StringTokenizer st2 = new StringTokenizer("Ala5Maja6Wieska7 Gienia8Ola","\\d");//Żaden konstruktor StringTokenizer nie przyjmuje wyrażenia regularnego jako delimiter. Taki zapis skompiluje się, ale argument"\\d" będzie interpetowany dosłownie a nie jako dowolna cyfra.
System.out.println(st2.countTokens()); //zwróci 1
System.out.println(st2.nextToken()); //Zwróci caly string "Ala5Maja6Wieska7 Gienia8Ola"
Klasy StringBuilder, StringBuffer
Te dwie klasy stosowane są w skrajnie różnych przypadkach, ale mają taką samą funkcjonalność – służą do składania wielu ciągów znaków w jeden.
Z punktu widzenia końcowego rezultatu, kod:
String name = ””; name += DAOCustomer.getName(); name += DAOCustomer.getSureName(); //Przyjmijmy, że obiekt DAOCustomer zwraca poszczególne dane klienta w postaci obiektu String String result = name;
będzie miał dokładnie takie same działanie jak:
StringBuilder name = ””; name.append(DAOCustomer.getName()); name.append(DAOCustomer.getSureName()); String result = name;
Po co więc ten StringBuilder? Ponieważ jak już wcześniej wspomniałem, String jest „immutable”, więc w pierwszym przypadku każda z linijek tworzy nam nowy obiekt „name” i alokuje dla niego miejsce w pamięci. Przy trzech linijkach nie jest to wielkim problemem, ale gdybyśmy do tej zmiennej zapragnęli dopisać jeszcze kilka innych danych i pomnożyli to przez kilka tysięcy klientów, to robi nam się spora dziura w pamięci.
Stąd kiedy wiemy, że dany string będzie „rozbudowywany” wiele razy, używamy obiektu klasy StringBuilder, który zawsze będzie jednym i tym samym obiektem, niezależnie od tego ile razy wykonamy na nim append.
Konstruktory StringBuilder:
StringBuilder() StringBuilder(CharSequence seq) StringBuilder(int capacity) StringBuilder(String str)
Na egzaminie radzę dobrze przyjrzeć się wywołaniu konstruktora StringBuildera, ponieważ Oracle lubi zastawiać bardzo niepozorną pułapkę:
StringBuilder sb1 = new StringBuilder(10); StringBuilder sb2 = new StringBuilder(„10”);
Jaka jest różnica pomiędzy sb1 i sb2?
Wywołanie sb1.toSting() zwróci nam pustego stringa, tylko sb2.toString() zwróci „10”.
Podając w konstruktorze wartość typu int definiujemy pojemność StringBuildera. Czyli na ile znaków początkowo StringBuilder zaalokował sobie pamięć. Oczywiście po takim zainicjowaniu możemy wstawić tam jedenaście lub więcej znaków, StringBuilder dynamicznie zwiększy swoją pojemność. Warto też pamiętać dwie rzeczy:
Po pierwsze jeśli nie podamy w konstruktorze pojemności, to domyślnie wynosi ona 16. Po drugie niezależnie od tego jaką pojemność podamy, wywołanie metody length() zwróci nam informację ile faktycznie znaków przechowuje obiekt, a nie jaka jest jego pojemność.
Podstawową metodą klasy StringBuilder jest append(), która jest przeciążona dla szeregu typów. Metoda ta dopisuje do StringBuildera ciąg znaków reprezentujący dany obiekt.
StringBuffer ma tą samą funkcjonlność co StringBuilder, ale dodatkowo jest „thread-safe” – to znaczy, że powinniśmy go stosować w pracy z wieloma wątkami, aby zagwarantować sobie spójność danych. Jeśli jednak pracujemy na jednym wątku, korzystamy z klasy StringBuilder, ponieważ działa ona szybciej.
Kilka przykładów:
StringBuilder sb1 = new StringBuilder();
System.out.println(sb1.capacity()); //Zwraca 16, domyślna pojemność StringBuildera
StringBuilder sb2 = new StringBuilder("Grzegorz");
System.out.println(sb2.capacity()); //Zwraca 24, czyli domyślne 16 + 8 znaków podanego w konstruktorze ciągu.
StringBuilder sb3 = new StringBuilder(10);
System.out.println(sb3.toString());// Zwraca pusty ciąg znaków, w konstruktorze podaliśmy inta czyli pojemność, a nie inicjalną treść obiektu.
System.out.println(sb3.capacity()); //Zwraca 10
sb3.append("1");
System.out.println(sb3.capacity()); //Zwraca 10, po zainicjowaniu pojemność jest zwiększa już tylko jeśli osiągniemy jej limit
sb3.append(sb2).append("tekst").append(sb3); //Do metody append możemy podać innego StringBuildera, możemy również poać tego samego StringBuildera, na którym wołamy metodę append.
System.out.println(sb3); // zwróci "1Grzegorztekst1Grzegorztekst"
Klasa Formatter
Z pomocą klasy Formatter możemy przekazać sformatowany tekst między innymi do obiektów reprezentujących pliki i strumienie. Formatowanie jest szczególnie użytecznie, kiedy chcemy wpleść dynamiczne dane pomiędzy stałe fragmenty tekstu. Zamiast zaśmiecać pamięć wieloma obiektami typu String składanymi na końcu w kolejny obiekt String reprezentujący całość, od razu możemy wygenerować końcowy ciąg znaków jedną metodą z odpowiednimi argumentami.
Konstuktory klasy Formatter:
Formatter()
Formatter(Appendable a)
Formatter(Appendable a, Locale l)
Formatter(File file)
Formatter(File file, String csn)
Formatter(File file, String csn, Locale l)
Formatter(Locale l)
Formatter(OutputStream os)
Formatter(OutputStream os, String csn)
Formatter(OutputStream os, String csn, Locale l)
Formatter(PrintStream ps)
Formatter(String fileName)
Formatter(String fileName, String csn)
Formatter(String fileName, String csn, Locale l)
Jak widać Formatter może zapisywać do:
- Plików – File, String.
- Strumieni – wszystkie implementacje OutputStream, w tym: ByteArrayOutputStream, FileOutputStream, FilterOutputStream, ObjectOutputStream, OutputStream, PipedOutputStream.
- Modyfikowalnych sekwencji znaków (Appendable) – BufferedWriter, CharArrayWriter, CharBuffer, FileWriter, FilterWriter, LogStream, OutputStreamWriter, PipedWriter, PrintStream, PrintWriter, StringBuffer, StringBuilder, StringWriter, Writer.
Dodatkowo już na poziomie konstruktora możemy określić dla jakiej lokalizacji ma być zainicjowany Formatter. Jeśli tego nie zrobimy, użyta zostanie lokalizacja zgodna z maszyną Javy, na której zostanie uruchomiony system.
Główną metodą tej klasy jest format():
Formatter format(Locale l, String format, Object... args)
Formatter format(String format, Object... args)
Do tej metody na początku możemy opcjonalnie przekazać Locale, zgodnie z którym będą formatowane dane takie jak np. liczby (różne kraje inaczej formatują liczby, separatorem dziesiętnym może być kropka lub przecinek, tysiące mogą być oddzielane przecinkami lub nie itd…). Locale to cały oddzielny rozdział wymagań do egzaminu, dlatego poświęcę temu obiektowi oddzielny artykuł.
W drugim argumencie przekazujemy nasz ciąg znaków, który ma być sformatowany. Możemy w nim użyć odpowiednich znaczników, które zostaną zastąpione konkretnymi danymi.
Trzeci argument to tzw „vararg”, możemy tam podać dowolną liczbę argumentów oddzielonych przecinkiem, a one zostaną przekazane w tablicy, jako jeden argument metody.
W tym przypadku podajemy właśnie referencje do tych obiektów, których treść ma zostać wpisana na miejsce naszych znaczników pozostawionych w formatowanym ciągu znaków.
Budowanie tych znaczników wydaje się skomplikowane, ale w rzeczywistości jest bardzo proste do opanowania. Szczegółowo opisałem je w kolejnym artykule z tej serii. Na tą chwilę wystarczy, że zapamiętasz najprostsze przykłady, każdy z poniższych znaczników w końcowym stringu zostanie zastąpiony przez treść zmiennej odpowiednio:
- %b – boolean
- %c – char
- %d – int, byte, shor lub long
- %f – float lub double
- %s – String
Może to brzmieć trochę zawile, ale wyjaśnię wszystko na prostym przykładzie:
Załóżmy, że iterując się przez listę użytkowników chcesz wygenerować prosty raport tekstowy, zawierający login użytkownika i wartość true/false w zależności od tego czy podał numer telefonu.
String login;
String phone;
Formatter formatter = new Formatter(New File(„raport.txt”));
for(User user: DAOUser.getAllUsers()){
login = user.getLogin();
phone = user.getPhone();
formatter.format(„%s;%b”, login, phone!=null);
}
formatter.flush();
formatter.close();
Żeby powyższy kod skompilował się, potrzebny byłby jeszcze poprawny obiekt DAOUser, ale postaram się zamieścić działającą wersję w repozytorium z przykładami do ćwiczeń.
Najważniejsze fragmenty to:
Zainicjonowanie formattera dla nowego pliku tekstowego:
Formatter formatter = new Formatter(New File(„raport.txt”));
W pętli wypisanie danych do pliku:
formatter.format(„%s;%b”, login, phone!=null);
W pierwszym argumencie poinformowaliśmy metodę, że:
Najpierw wstawiamy ciąg znaków, metoda bierze sobie pierwszy z listy przekazanych jej obiektów, czyli String login i zastępuje jego treścią znacznik %s.
Potem mamy średnik, który nie jest żadnych znakiem spacjalnym. Zostanie po prostu wstawiony do końcowego tekstu.
Na końcu poprzez znacznik %b informujemy metodę, że ma tam wstawić wartość true lub false. Metoda bierze zatem kolejny obiekt z listy argumentów. Znajduje się tam warunek logiczny phone!=null. Jeżeli zmienna phone zawiera jakiś ciąg znaków, warunek logiczny będzie prawdziwy i do budowanego ciągu znaków dopisany zostanie tekst „true”.
Ostatecznie dla przykładowych danych:
login = „Stefan”;
phone = „0700 888 101”;
login = „Roman”;
phone = null;
Do pliku zostaną zapisane linijki: „Stefan;true” oraz „Roman;false”. Bez cudzysłowów.
Na końcu kodu wołane są dwie funkcje: flush() oraz close(). O ile ta druga nie wymaga zbędnych wyjaśnień- po prostu zamyka plik, do którego pisaliśmy, to metoda flush nie dla wszystkich musi mieć jasne zastosowanie.
Otóż ze względów wydajnościowych, kiedy każemy formaterowi (i nie tylko, tyczy się to większości obiektów zapisujących do plików) zapisać jakiś ciąg znaków do pliku, to zapis NIE odbywa się od razu. Java buforuje sobie w pamięci dane, które ma zapisać i dopiero jak uzbiera odpowiednio dużo, to za jednym razem „zrzuca” całość do pliku, redukując w ten sposób ilość bardzo kosztownych operacji, jakimi są zapisy na dysk. Wołając metodę flush() wymuszamy fizyczne zapisanie do pliku zawartości bufora, bez względu na to, czy już się zapełnił, czy nie – dzięki temu mamy pewność, że do pliku zostało zapisane absolutnie wszystko co miało być zapisane.
Kilka przykładów:
Formatter f;
String text = "Konstantynopolitanczykowianeczka";
f = new Formatter();
System.out.println(f.format("%b", text)); //Zwróci true, mimo, że użyliśmy stringa, to znacznik %b będzie rzutował tego stringa na typ boolean
f = new Formatter();
System.out.println(f.format("%b", null)); //Zwróci false;
f = new Formatter();
System.out.println(f.format("%b", new Boolean("TrUe"))); //Zwróci true, przy okazji warto zauważyć, że w konstruktorze Boolean wielkość liter nie ma znaczenia
f = new Formatter();
System.out.println(f.format("%b", -10)); //Zwróci true
f = new Formatter();
System.out.println(f.format(Locale.UK,"%f", 1234.99d)); //Zwróci 1234.990000 dla Locale United Kingdom separatorem dzisiętnym jest kropka
f = new Formatter();
System.out.println(f.format(Locale.CANADA_FRENCH,"%f", 1234.99d));// Zwróci 1234,990000, dla Locale Kanady separatorem jest przecinek
f = new Formatter();
//System.out.println(f.format(Locale.CANADA_FRENCH,"%d", 1234.99d)); // Zwróci wyjątek IllegalFormatConversionException. Znacznik %d oznacza liczbę całkowitą i niedopuszczalne jest podanie dla niego typu float lub double
//Dlaczego przed każdym formatowaniem inicjuje na nowo formatter?
//Jeśli wywołasz na jednym obiekcie typu Formatter metodę format kilka razy, jej rezultaty będą się do siebie dodawać:
f = new Formatter();
f.format("Wywyłanie metody nr: %d ", 1);
f.format("Wywyłanie metody nr: %d ", 2);
System.out.println(f.toString()); // zwróci "Wywyłanie metody nr: 1 Wywyłanie metody nr: 2 "
Na koniec, pamiętaj, że jestem tylko człowiekiem. Jeśli zauważyłeś jakiś błąd poinformuj mnie o tym proszę. Będę również wdzięczny za każdą konstruktywną krytykę.