Interfejs Supplier opisałem już pewien czas temu. Dziś chciałbym przedstawić pewne specyficzne zastosowanie tego interfejsu. Mianowicie stworzymy klasę, która będzie w oparciu o ten interfejs odczytywać pliki.

Prawie leniwe czytanie plików

Generalnie w Javie da się czytać pliki w sposób leniwy. Bierzemy sobie BufferedReader i wpinamy go w jakiś listener. Następnie uderzamy zdarzeniem „następna linia” i pobieramy linię. I tak aż się plik skończy.
W przypadku Supplier-a mechanika działania jest bardzo zbliżona. Z jedną małą różnicą. Musimy czytać „do przodu” jeżeli chcemy by odczyt był następnie wykorzystywany w ramach iteratorów. Te ostatnie zapewnią nam spójny interfejs z Guavą.
Co więcej o ile w przypadku „zwykłego” czytania plików możemy sobie zaryzykować zwrócenie wartości null lub wymuszenie obsługi weryfikowalnego wyjątku to gdy idziemy w kierunku idiomów funkcyjnych dobrze by było zamknąć jakoś te problemy.

Catched Exceptions i Guava

Dziś na skróty. Generalnie jedną z cech programowania funkcyjnego jest brak efektów ubocznych. Jednym z takich efektów jest rzucenie wyjątku. Wszystko ładnie pięknie, ale uruchommy sobie interpreter Erlanga (który językiem funkcyjnym jest) i napiszmy coś takiego:

Listing 1. „Brak efektów ubocznych”

Wynik = Liczba / 0.

Oczywiście dostaniemy wyjątek. Jako, że Erlang hołduje idei „Let it crash” to jeżeli doprowadzilibyśmy do takiej sytuacji w programie to maszyna wirtualna by ubiła nam proces, a następnie go wznowiła.
My nie będziemy aż tak mili. Jeżeli coś się wywali to przepchniemy to na chwilę obecną do wyjątku nieweryfikowalnego i wyrzucimy… Let it crash 😀

Przy czym należy pamiętać, że Guava udostępnia klasę narzędziową Throwables, która pozwala na opakowywanie i konwertowanie wyjątków. Tu będzie ona średnio przydatna 😉

Czytamy plik linia po linii z użyciem Supplier

Zakładam, że czytamy linia po linii, plik np. csv, a nie plik binarny, gdzie chcemy odczytać blok. Ogólne zasady będą takie same komplikuje się tylko zasady określające ile danych chcemy odczytać.

Zastanówmy się przez chwilę co powinno być efektem przeczytania linii z pliku? Zapewne obiekt typu String. Co jeżeli linia jest pusta? Nasz String powinien być pusty. W przypadku końca pliku będzie oczywiście null. Czy to nam odpowiada? Raczej nie. Szczególnie ten ostatni przypadek. Co prawda można by zamienić do na zwracanie pustego stringa, ale wtedy nie wiemy, że odczytujemy ostatnią linię, a nie np. pustą linię w środku pliku.

Pamiętajmy, że odczyt pliku będzie obudowany w dostawcę, a ten nie dostarcza metod iteratora typu hasNext. Oczywiście można napisać taki iterator (kolejny post będzie o tym). Jednak my skupimy się na samym odczycie.

Jak wybrnąć z tego problemu? Użyć Optional. To było proste. Zasady też są proste. Jeżeli dojdziemy do końca pliku to zwracamy Absent inaczej Present. Do odczytu plików tekstowych linia po linii posłuży nam stary dobry BufferedReader. WIemy zatem jak będzie wyglądał „core” naszej aplikacji

Listing 2. Metoda get naszego readera

public class FileReaderViaSupplier implements Supplier<Optional<String>> {

	private BufferedReader reader;

	@Override
	public Optional<String> get() {
		try {
			String reference = reader.readLine(); // 1
			return Optional.fromNullable(reference).or(Optional.<String>absent()); // 2
		} catch (IOException e) {
			return Optional.absent(); // 3
		}
	}
}

Na początku odczytujemy pojedynczą linię (1), a następnie na jej bazie tworzymy obiekt Optional, który będzie istniał o ile tylko odczytaliśmy coś co nie jest null-em. W przeciwnym wypadku zostanie zwrócony Absent. Absent zostanie zwrócone też w przypadku błędu.

Jak to zainicjować

Mamy co prawda reader, ale nadal nie mamy kodu odpowiedzialnego za jego inicjalizację. Przedstawię tylko jeden konstruktor. Można go potraktować jako ostatni w ścieżce delegacji pomiędzy różnymi wersjami (bo jak zwykle operując na plikach mamy od cholery konstruktorów).

Listing 3. Konstruktor naszego readera

public FileReaderViaSupplier(File file, Charset charset) {
	checkNotNull(file);
	checkNotNull(charset);
	checkArgument(file.exists(), "File %s does not exist", file.getAbsolutePath());
	try {
		reader = Files.asCharSource(file, charset).openBufferedStream();
	} catch (IOException e) {                                                                    
		checkState(false, e.getMessage());
	}
}

W pierwszych trzech liniach sprawdzamy czy w ogóle możemy sobie pozwolić na utworzenie readera. Wykorzystuję tu Preconditions, które opisywałem pewien czas temu. Następnie próbujemy utworzyć BufferedReader. Jeżeli to się nie uda to walimy kolejnym wyjątkiem.

W przeciwieństwie do odczytu gdzie błąd oznaczał tylko zakończenie działania tu przyjąłem trochę inną strategię. Proces odczytu jest leniwy. Zatem możemy zabezpieczyć się przed warunkami zewnętrznymi inaczej niż w przypadku zachłannego procesu tworzenia obiektu. Tu chcemy od razu powiedzieć „nie wiem, nie znam się, zarobiony jestem, przyjdź pan jurto”. Natomiast w przypadku odczytu po prostu mówimy „nic więcej nie mogę odczytać”. Nie ważne jest czy wynika to z problemów z plikiem czy też jest po prostu koniec pliku.

Podsumowanie

Mamy zatem podstawowy element pozwalający na leniwe odczytywanie plików. W dodatku napisaliśmy to w sposób pozwalający na wykorzystanie razem z innymi elementami Guavy.

O tym jak dobrać rozwiązanie do problemu i czy opłaca się pisać rozwiązania uogólnione będę mówił w trakcie warsztatów “Beginning Functional Programming in Java world” na Warsjawie już 26-27 września.

Na koniec mała prośba jeżeli spodobał ci się ten wpis udostępnij go w mediach społecznościowych korzystając z przycisków poniżej. Jak masz pytania zadaj je w komentarzu. Postaram się zaspokoić twoją ciekawość.