Към съдържанието

Тема 5 — Параметризирани типове (Generics). Изключения.

Две от най-важните добавки в Java 5.0 са generics и унифицираният механизъм за изключения. Generics решават хроничен проблем на ранните Java колекции — загубата на типова информация при запис — и правят грешките при привеждане на типове компилационни, а не изпълнителни. Изключенията, за разлика от C++, са задължителна архитектурна единица: компилаторът изисква тяхната декларация и обработка, а try-with-resources в Java 7 елиминира целяш клас от ресурсни течове.

1. Параметризирани типове (Generics)

1.1 Мотивация

Преди Java 5.0 колекциите съхраняват Object референции. Следствие — задължително привеждане при извличане с риск от ClassCastException:

// Без generics (Java 1.4):
Collection dates = new ArrayList();
dates.add(new Date());
Date d = (Date) dates.iterator().next(); // ако не е Date — изключение при изпълнение

С generics типовата грешка се открива при компилация:

Collection<Date> dates = new ArrayList<Date>();
dates.add(new Date()); // OK
dates.add("foo"); // ГРЕШКА при компилация!
Date d = dates.iterator().next(); // без привеждане

Шаблонната променлива (T, E, V и др.) може да се използва на три места в дефиницията на класа:

  • при деклариране на полета (T ob;);
  • като тип на параметри на методи (Gen(T o) { ... });
  • като връщан тип на методи (T getob() { ... }).

1.2 Собствен параметризиран клас

class Gen<T> {
T ob;
Gen(T o) { ob = o; }
T getob() { return ob; }
void showType() {
System.out.println("Типът T е " + ob.getClass().getName());
}
}

Употреба:

Gen<Integer> iOb = new Gen<Integer>(55); // boxing: int → Integer
iOb.showType(); // java.lang.Integer
int v = iOb.getob(); // без привеждане; unboxing
Gen<String> strOb = new Gen<String>("Тест");
String str = strOb.getob();

Правила:

  • Работи само с референтни типовеGen<int> е недопустимо; ползва се Gen<Integer>;
  • Gen<Integer> и Gen<Double> са различни типове при компилация.

1.2.1 Еквивалентен клас без шаблон (NonGen)

За да се онагледи ползата от generics, разгледаме клас NonGen, функционално идентичен на Gen<T>, но реализиран с Object вместо шаблонен параметър:

class NonGen {
Object ob;
NonGen(Object o) {
ob = o;
}
Object getob() {
return ob;
}
void showType() {
System.out.println("Типът е " + ob.getClass().getName());
}
}

Употребата изисква задължително привеждане при всяко извличане и не осигурява типова проверка при компилация:

NonGen iOb = new NonGen(55);
int v = (Integer) iOb.getob(); // задължително привеждане
NonGen strOb = new NonGen("Тест");
String str = (String) strOb.getob();
// Грешен код, компилира се успешно, но пада при изпълнение:
iOb = strOb;
v = (Integer) iOb.getob(); // ClassCastException по време на изпълнение!

С Gen<T> тази грешка е невъзможна — компилаторът я открива още преди стартиране.

1.3 Клас с повече шаблонни параметри

class TwoGen<T, V> {
T ob1;
V ob2;
TwoGen(T o1, V o2) {
ob1 = o1;
ob2 = o2;
}
T getob1() { return ob1; }
V getob2() { return ob2; }
boolean isSameReference(TwoGen<T, V> arg) {
if (ob1 == arg.ob1 && ob2 == arg.ob2) {
return true;
} else {
return false;
}
}
}
// Java 7+ — diamond operator <> извежда типовете:
TwoGen<Integer, String> tg = new TwoGen<>(55, "Шаблони");

Методът isSameReference демонстрира предаването на параметризиран тип като аргумент на метод — сигнатурата TwoGen<T, V> arg задължава аргументът да има точно същите шаблонни типове:

TwoGen<Integer, String> tgObj = new TwoGen<>(55, "Шаблони");
if (tgObj.isSameReference(new TwoGen<>(66, "test"))) {
System.out.println("Same reference");
}

1.4 Ограничаване на шаблона

Горна граница (extends) — допуска само Number и неговите наследници:

class Stats<T extends Number> {
T[] nums;
Stats(T[] o) { nums = o; }
double average() {
double sum = 0.0;
for (T n : nums) {
sum += n.doubleValue();
}
return sum / nums.length;
}
}
Stats<Integer> iob = new Stats<>(new Integer[]{1, 2, 3, 4, 5});
// Stats<String> strob = new Stats<>(...); // компилационна грешка

Метасимволът ? — за предаване на шаблонен клас, допускащ всеки наследник:

boolean sameAvg(Stats<?> ob) {
return average() == ob.average();
}

Комбиниране с интерфейс:

class Gen<T extends MyClass & MyInterface> { }

1.5 Наследяване на параметризиран клас

class Gen2<T> extends Gen<T> {
Gen2(T obj) { super(obj); }
@Override
T getob() {
System.out.print("Gen2.getob(): ");
return ob;
}
}

2. Изключения

2.1 Разлики от C++

В C++ изключенията са незадължително разширение. В Java те са вградена архитектурна единица:

  • Сигнатурата на метода обявява изключенията с throws;
  • Компилаторът изисква обработка или деклариране (checked exceptions — проверявани изключения);
  • Пропускането е компилационна грешка.

2.2 Механизъм на действие

При изхвърляне на изключение:

  1. В heap се създава обект на изключение с new;
  2. Текущото изпълнение се прекъсва;
  3. JVM търси подходящ catch блок в стека на повикванията;
  4. Намереният обработчик поема управлението.
if (testRef == null) {
throw new NullPointerException("testRef is null");
}

NullPointerException е unchecked exception — не изисква деклариране с throws.

2.3 try-catch-finally

try {
// код, генериращ изключения
} catch (Type1 id1) {
// обработка на Type1
} catch (Type2 id2) {
// обработка на Type2
} catch (Exception e) { // най-общият — последен
System.out.println("Caught: " + e.getMessage());
} finally {
// изпълнява се ВИНАГИ — независимо от изключение или нормален изход
}

Редът на catch блоковете е от по-специфичен към по-общ — наследниците трябва да са преди базовия клас.

2.4 Методи на Throwable

e.getMessage() // текст на съобщението
e.toString() // кратко описание (тип + съобщение)
e.printStackTrace() // стек на повикванията
e.getClass().getName() // клас на изключението

2.5 Модели на обработка

Прекратяване (termination) — поддържан в Java и C++: грешката е критична; изпълнението се прекратява и управлението се предава на обработчика.

Продължаване (resumption) — имплементира се с while + try:

boolean ok = false;
while (!ok) {
try {
riskyOperation();
ok = true;
} catch (SomeException e) {
correctAndRetry();
}
}

2.6 Прихващане по наследство

class BaseException extends Exception {}
class DerivatedException extends BaseException {}
try {
throw new DerivatedException();
} catch (DerivatedException s) { // по-специфичен — ПЪРВИ
System.out.println("Caught DerivatedException");
} catch (BaseException a) { // по-общ — след него
System.out.println("Caught BaseException");
}

2.7 Стандартни изключения

Йерархия: ThrowableError | ExceptionRuntimeException и конкретни класове.

Ключовото разграничение е между checked и unchecked изключения:

ВидБазов класthrows декларацияКога се ползва
CheckedExceptionЗадължителнаОчаквани външни грешки (файл, мрежа)
UncheckedRuntimeExceptionНе се изискваПрограмни грешки (null, грешен индекс)
ErrorErrorНе се изискваСистемни грешки — не се прихващат

Checked exception — компилаторът изисква throws в сигнатурата или catch блок на извикващия:

// Методът трябва да декларира throws — иначе компилационна грешка
public String readFile(String path) throws IOException {
return Files.readString(Path.of(path));
}
// Извикващият трябва да обработи:
try {
String content = readFile("data.txt");
} catch (IOException e) {
System.out.println("Файлът не е намерен: " + e.getMessage());
}

RuntimeException (unchecked) — не изисква throws в сигнатурата; компилаторът не проверява дали се обработва:

// Без throws — компилаторът не го изисква
public int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("Деление на нула");
}
return a / b;
}
// Прихващането е по избор — ако не се прихване, JVM прекратява нишката
try {
int result = divide(10, 0);
} catch (ArithmeticException e) {
System.out.println("Грешка: " + e.getMessage());
}

Често срещани unchecked exceptions: NullPointerException, ArrayIndexOutOfBoundsException, ClassCastException, ArithmeticException, IllegalArgumentException, IllegalStateException.

Error — критични системни грешки, не се прихващат в нормален код:

  • OutOfMemoryError, StackOverflowError.

2.8 Собствени класове изключения

class MyException extends Exception {
public MyException() {}
public MyException(String msg) { super(msg); }
}
public class Inheriting {
public static void f() throws MyException {
throw new MyException();
}
public static void main(String[] args) {
try {
f();
} catch (MyException e) {
e.printStackTrace();
}
}
}

2.9 Множествен catch (JDK 7+)

Когато различни типове изключения се обработват идентично:

try {
// ...
} catch (EOFException | FileNotFoundException e) {
System.out.println("File exception: " + e.getMessage());
} catch (IOException e) {
System.out.println("IO exception");
}

2.10 try-with-resources (JDK 7+)

Ресурсите, имплементиращи AutoCloseable, се затварят автоматично при излизане от блока try — дори при изключение:

try (RandomAccessFile fin = new RandomAccessFile("src/FileTest.java", "r")) {
String str;
while ((str = fin.readLine()) != null) {
System.out.println(str);
}
// fin.close() не е необходимо
} catch (EOFException | FileNotFoundException e) {
System.out.println("File exception");
} catch (IOException e) {
System.out.println("IO exception");
}

Множество ресурси:

try (Scanner s1 = new Scanner(new File(fileNameBooks));
Scanner s2 = new Scanner(new File(fileNameReaders))) {
// работа с s1 и s2
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// s1 и s2 се затварят автоматично — в обратен ред на декларацията

Предимства: предотвратява изтичане на ресурси; кодът е по-кратък; гаранция за затваряне дори при вложени изключения.

Резюме

  • Generics добавят типова безопасност при компилация и елиминират ръчното привеждане при работа с колекции и шаблонни класове.
  • Параметризираните типове работят само с референтни типове; за прости типове се ползват wrapper класовете с autoboxing/unboxing.
  • В Java изключенията са задължителна архитектурна единица — компилаторът изисква обявяване и обработка.
  • try-catch-finally осигурява структуриран механизъм; finally блокът се изпълнява винаги.
  • try-with-resources (JDK 7+) автоматично затваря ресурси и е предпочитаният начин за работа с файлове и потоци.
  • Множественият catch (JDK 7+) позволява обединяване на обработчици за различни типове изключения.