Тема 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()); // OKdates.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 → IntegeriOb.showType(); // java.lang.Integerint 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 Механизъм на действие
При изхвърляне на изключение:
- В heap се създава обект на изключение с
new; - Текущото изпълнение се прекъсва;
- JVM търси подходящ
catchблок в стека на повикванията; - Намереният обработчик поема управлението.
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 Стандартни изключения
Йерархия: Throwable → Error | Exception → RuntimeException и конкретни класове.
Ключовото разграничение е между checked и unchecked изключения:
| Вид | Базов клас | throws декларация | Кога се ползва |
|---|---|---|---|
| Checked | Exception | Задължителна | Очаквани външни грешки (файл, мрежа) |
| Unchecked | RuntimeException | Не се изисква | Програмни грешки (null, грешен индекс) |
| Error | Error | Не се изисква | Системни грешки — не се прихващат |
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+) позволява обединяване на обработчици за различни типове изключения.