Недеља 11 — Датотеке и изузеци

Код: GitHub · преузми пројекат (ZIP) · задатак за самостални рад

Ове недеље спајамо две теме које природно припадају заједно:

  • рад са датотекама;
  • обраду грешака помоћу изузетака.

Уместо да try/catch учимо на вештачким примерима, користимо реалан проблем:

учитати податке из датотеке, проверити их и одлучити шта радити када нешто није исправно.

Главни домен је списак резервација путовања.

Један ред фајла изгледа овако:

ime,prezime,godine,destinacija

На пример:

Ana,Jovanovic,22,Rim
Marko,Petrovic,abc,Pariz
Milica,Ilic,31,Atina

Други ред садржи грешку: abc није број година.


Циљеви

После вежби студент треба да уме да:

  • чита текстуалну датотеку ред по ред;
  • разуме зашто рад са фајловима може да произведе грешке које не можемо унапред спречити;
  • користи try/catch;
  • разуме разлику између хватања и прослеђивања изузетка;
  • користи throws;
  • направи сопствени изузетак за грешку домена;
  • користи try-with-resources;
  • разуме зашто ресурс попут фајла треба поуздано затворити;
  • разликује техничку грешку од грешке у садржају података;
  • донесе одлуку да ли неисправан ред треба прескочити или прекинути цео процес.

Редослед примера

primer01_citanje_fajla

Прво само читамо фајл:

BufferedReader
FileReader
readLine()

Циљ није да учимо целу java.io библиотеку.

Довољно је разумети:

otvori fajl
čitaj red po red
zatvori fajl

Одмах се појављује питање:

Шта ако фајл не постоји?

primer02_try_catch

Отварање и читање фајла може да баци IOException.

Користимо:

try {
    ...
} catch (IOException e) {
    ...
}

Поента није “ухвати све”.

Поента је:

Овај део програма зна како да реагује на конкретну грешку.

primer03_parsiranje_i_vise_gresaka

Сада читамо CSV ред и претварамо:

godine

у int.

Можемо добити различите проблеме:

  • IOException — проблем са фајлом;
  • NumberFormatException — податак није број;
  • погрешан број поља — лош формат реда.

То су различите грешке и не морају све да се третирају исто.

primer04_sopstveni_izuzetak

Уводи се:

NeispravanRedException

Зашто?

Зато што желимо да код који парсира резервацију каже:

Овај ред не представља исправну резервацију.

То је прецизније од генеричког Exception.

Метода:

Rezervacija parsiraj(String red)

може да има:

throws NeispravanRedException

Тиме јасно комуницира део свог уговора.

primer05_try_with_resources

Ручно затварање:

reader.close();

је лако заборавити, нарочито ако се између отварања и затварања деси изузетак.

Зато користимо:

try (BufferedReader reader = ...) {
    ...
}

Ресурс се затвара аутоматски.

Ово је преферирани начин за ресурсе који имплементирају AutoCloseable.

primer06_odgovornost_za_gresku

Најважнији пример.

Метода која парсира један ред:

parsira red

не мора да одлучује:

da li da prekine ceo program
da li da preskoči red
da li da prikaže upozorenje

То је одлука вишег нивоа.

На пример:

UcitajRezervacije

може да одлучи:

неисправан ред прескачемо, али настављамо са остатком фајла.

Овде се лепо види раздвајање одговорности.


Техничке и доменске грешке

Користан ментални модел:

Техничка грешка

Примери:

fajl ne postoji
nema dozvole za čitanje
disk problem

Типично долазе из библиотеке, нпр. IOException.

Доменска грешка

Примери:

godine su negativne
nedostaje destinacija
red nema očekivani broj kolona

То су правила нашег програма.

За њих често има смисла сопствени изузетак:

NeispravanRedException

throw и throws

throw

Заиста бацамо објекат изузетка:

throw new NeispravanRedException("...");

throws

У потпису методе најављујемо да метода може да проследи одређени изузетак:

public static Rezervacija parsiraj(String red)
        throws NeispravanRedException

Ове две ствари не треба мешати.


Checked и unchecked изузеци

Java разликује две велике групе.

Checked

Пример:

IOException

Компајлер захтева да га:

  • ухватимо;
  • или проследимо помоћу throws.

Unchecked

Пример:

NumberFormatException
IllegalArgumentException
NullPointerException

Компајлер нас не тера да их наведемо у потпису.

За овај курс није циљ да памтимо читаву хијерархију изузетака.

Важно је разумети:

ко може да поправи проблем и на ком нивоу га треба обрадити.


Зашто не catch (Exception e) свуда?

Зато што такав catch често сакрије разлику између потпуно различитих проблема.

Боље је да код јасно говори:

catch (IOException e)

или:

catch (NeispravanRedException e)

када заиста знамо шта са тим проблемом треба урадити.

Широки catch (Exception e) понекад има смисла на границама апликације, али није добар подразумевани стил за пословну логику.


finally

finally треба објаснити јер је део језика:

try {
    ...
} finally {
    ...
}

Код из finally блока се извршава без обзира на то да ли је дошло до изузетка.

Али за затварање фајлова данас ћемо углавном користити:

try-with-resources

јер је безбедније и читљивије.


Питања за дискусију

  1. Зашто метода за читање фајла може да не успе иако је наш код исправан?
  2. Која је разлика између throw и throws?
  3. Када има смисла да метода ухвати изузетак, а када да га проследи?
  4. Зашто NumberFormatException не морамо да наведемо у throws?
  5. Зашто сопствени NeispravanRedException може бити бољи од обичног Exception?
  6. Да ли парсер једног реда треба да одлучује да ли се прекида цео програм?
  7. Ко треба да одлучи да ли се неисправан ред прескаче?
  8. Зашто је try-with-resources бољи од ручног close()?
  9. Шта се дешава са ресурсом ако се изузетак деси усред try блока?
  10. Зашто није добро свуда користити catch (Exception e)?
  11. Да ли свака неисправна вредност мора да се моделује изузетком?
  12. Када је обичан if бољи од бацања изузетка?

Мини задатак

У ZADATAK.md налази се учитавање списка пријављених студената из фајла.

Задатак комбинује:

  • читање фајла;
  • парсирање;
  • сопствени изузетак;
  • прескакање лоших редова;
  • колекцију из претходне недеље.

Допуне: упис, формат и assert

primer07_pisanje одваја учитавање од писања текстуалног извештаја. Радни директоријум је фолдер пројекта, не фолдер класе. Улаз и излаз користе UTF-8. Завршни парсер задржава све провере из претходног примера. split(",", -1) чува завршна празна поља; ово није пун CSV парсер (нема quoting/escaping подршке).

primer08_assert показује интерну тврдњу и покретање са -ea. Тврдње нису замена за валидацију јавних аргумената јер могу бити искључене. Експлицитно бачен AssertionError у проверама из недеље 13 не зависи од тог прекидача.