CodeGym /Java блог /Случаен /Java Generics: как да използвате ъглови скоби на практика...
John Squirrels
Ниво
San Francisco

Java Generics: как да използвате ъглови скоби на практика

Публикувано в групата

Въведение

Започвайки с JSE 5.0, генериците бяха добавени към арсенала на езика Java.

Какво представляват генериците в java?

Generics са специалният механизъм на Java за прилагане на генерично програмиране - начин за описание на данни и алгоритми, който ви позволява да работите с различни типове данни, без да променяте описанието на алгоритмите. Уебсайтът на Oracle има отделен урок, посветен на генеричните продукти: „ Урок “. За да разберете генеричните лекарства, първо трябва да разберете защо са необходими и Howво дават. В раздела „ Защо да използваме Generics? “ на урока се казва, че няколко цели са по-силна проверка на типа по време на компorране и премахване на необходимостта от изрични преобразувания. Generics в Java: How да използвате ъглови скоби на практика - 1Нека се подготвим за някои тестове в нашия любим онлайн компилатор на Java Tutorialspoint . Да предположим, че имате следния code:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello");
		String text = list.get(0) + ", world!";
		System.out.print(text);
	}
}
Този code ще работи перфектно. Но Howво ще стане, ако шефът дойде при нас и каже, че "Здравей, свят!" е прекалено използвана фраза и че трябва да върнете само „Здравей“? Ще премахнем codeа, който свързва ", свят!" Това изглежда достатъчно безобидно, нали? Но всъщност получаваме грешка ПО ВРЕМЕ НА КОМПИЛИРАНЕ:

error: incompatible types: Object cannot be converted to String
Проблемът е, че в нашия списък се съхраняват обекти. String е наследник на Object (тъй като всички Java класове имплицитно наследяват Object ), което означава, че се нуждаем от изрично преобразуване, но не сме добавor такова. По време на операцията за конкатенация, статичният метод String.valueOf(obj) ще бъде извикан с помощта на обекта. В крайна сметка той ще извика метода toString на класа Object . С други думи, нашият списък съдържа Object . Това означава, че когато имаме нужда от конкретен тип (не Object ), ще трябва сами да направим преобразуването на типа:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + (String)str);
		}
	}
}
В този случай обаче, тъй като List приема обекти, той може да съхранява не само String s, но и Integer s. Но най-лошото е, че компилаторът не вижда нищо нередно тук. И сега ще получим грешка ПО ВРЕМЕ НА ИЗПЪЛНЕНИЕ (известна като „грешка по време на изпълнение“). Грешката ще бъде:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Трябва да се съгласите, че това не е много добре. И всичко това, защото компилаторът не е изкуствен интелект, способен винаги правилно да отгатне намерението на програмиста. Java SE 5 въведе генерични codeове, за да ни позволи да кажем на компилатора за нашите намерения - за това кои типове ще използваме. Поправяме нашия code, като казваме на компилатора Howво искаме:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList<>();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + str);
		}
	}
}
Както можете да видите, вече не се нуждаем от преобразуване към String . Освен това имаме ъглови скоби около аргумента тип. Сега компилаторът няма да ни позволи да компorраме класа, докато не премахнем реда, който добавя 123 към списъка, тъй като това е Integer . И това ще ни каже. Много хора наричат ​​генеричните лекарства „синтактична захар“. И те са прави, тъй като след като генериците се компorрат, те наистина стават преобразувания от същия тип. Нека да разгледаме byte codeа на компorраните класове: един, който използва изрично предаване и един, който използва генерични codeове: Generics в Java: How да използвате ъглови скоби на практика - 2След компилация всички генерични codeове се изтриват. Това се нарича " изтриване на типа". Изтриването на типове и генериците са проектирани да бъдат обратно съвместими с по-старите версии на JDK, като същевременно позволяват на компилатора да помага с дефинициите на типове в новите версии на Java.

Сурови видове

Говорейки за генерични продукти, винаги имаме две категории: параметризирани типове и необработени типове. Необработените типове са типове, които пропускат „изясняване на типа“ в ъглови скоби: Generics в Java: How да използвате ъглови скоби на практика - 3Параметризираните типове, от страна, включват „изясняване“: Generics в Java: How да използвате ъглови скоби на практика - 4Както можете да видите, използвахме необичайна конструкция, маркирана със стрелка на екранната снимка. Това е специален синтаксис, добавен към Java SE 7. Нарича се „ диамант “. Защо? Ъгловите скоби образуват диамант: <> . Трябва също така да знаете, че диамантеният синтаксис е свързан с концепцията за „ извод на типа “. В крайна сметка компилаторът, виждайки <>отдясно, разглежда лявата страна на оператора за присвояване, където намира типа на променливата, чиято стойност се присвоява. Въз основа на това, което намира в тази част, той разбира типа на стойността вдясно. Всъщност, ако генеричен тип е даден отляво, но не и отдясно, компилаторът може да изведе типа:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList();
		list.add("Hello, World");
		String data = list.get(0);
		System.out.println(data);
	}
}
Но това смесва новия стил с генерични лекарства и стария стил без тях. А това е крайно нежелателно. Когато компorраме горния code, получаваме следното съобщение:

Note: HelloWorld.java uses unchecked or unsafe operations
Всъщност причината, поради която дори трябва да добавите диамант тук, изглежда неразбираема. Но ето един пример:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = Arrays.asList("Hello", "World");
		List<Integer> data = new ArrayList(list);
		Integer intNumber = data.get(0);
		System.out.println(data);
	}
}
Ще си спомните, че ArrayList има втори конструктор, който приема колекция като аргумент. И тук се крие нещо зловещо. Без диамантения синтаксис компилаторът не разбира, че е измамен. Със синтаксиса на диаманта става. И така, правило №1 е: винаги използвайте диамантения синтаксис с параметризирани типове. В противен случай рискуваме да пропуснем мястото, където използваме необработени типове. За да елиминираме предупрежденията „използва непроверени or опасни операции“, можем да използваме анотацията @SuppressWarnings(„unchecked“) за метод or клас. Но помислете защо сте решor да го използвате. Запомнете правило номер едно. Може би трябва да добавите аргумент тип.

Общи методи на Java

Generics ви позволяват да създавате методи, чиито типове параметри и тип връщане са параметризирани. Отделен раздел е посветен на тази възможност в ръководството на Oracle: „ Общи методи “. Важно е да запомните синтаксиса, преподаван в този урок:
  • включва списък с параметри на типа в ъглови скоби;
  • списъкът с параметри на типа е преди връщания тип на метода.
Да разгледаме един пример:

import java.util.*;
public class HelloWorld {
	
    public static class Util {
        public static <T> T getValue(Object obj, Class<T> clazz) {
            return (T) obj;
        }
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList("Author", "Book");
		for (Object element : list) {
		    String data = Util.getValue(element, String.class);
		    System.out.println(data);
		    System.out.println(Util.<String>getValue(element));
		}
    }
}
Ако погледнете класа Util , ще видите, че той има два общи метода. Благодарение на възможността за извеждане на типа можем or да посочим типа директно на компилатора, or можем да го посочим сами. И двата варианта са представени в примера. Между другото, синтаксисът има много смисъл, ако се замислите. Когато декларираме общ метод, ние указваме параметъра на типа ПРЕДИ метода, защото ако декларираме параметъра на типа след метода, JVM няма да може да разбере кой тип да използва. Съответно първо декларираме, че ще използваме параметъра тип T и след това казваме, че ще върнем този тип. Естествено Util.<Integer>getValue(element, String.class) ще се провали с грешка:несъвместими типове: Class<String> не може да бъде преобразуван в Class<Integer> . Когато използвате общи методи, винаги трябва да помните изтриването на типа. Да разгледаме един пример:

import java.util.*;
public class HelloWorld {
	
    public static class Util {
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList(2, 3);
		for (Object element : list) {
		    System.out.println(Util.<Integer>getValue(element) + 1);
		}
    }
}
Това ще работи добре. Но само докато компилаторът разбира, че типът връщане на извикания метод е Integer . Заменете оператора за изход на конзолата със следния ред:

System.out.println(Util.getValue(element) + 1);
Получаваме грешка:

bad operand types for binary operator '+', first type: Object, second type: int.
С други думи, възникнало е изтриване на типа. Компилаторът вижда, че никой не е посочил типа, така че типът е посочен като Object и методът се проваля с грешка.

Генерични класове

Не само методите могат да бъдат параметризирани. Класовете също могат. Разделът "Общи типове" на урока на Oracle е посветен на това. Да разгледаме един пример:

public static class SomeType<T> {
	public <E> void test(Collection<E> collection) {
		for (E element : collection) {
			System.out.println(element);
		}
	}
	public void test(List<Integer> collection) {
		for (Integer element : collection) {
			System.out.println(element);
		}
	}
}
Тук всичко е просто. Ако използваме общия клас, параметърът тип се посочва след името на класа. Сега нека създадем екземпляр на този клас в основния метод:

public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
Този code ще работи добре. Компилаторът вижда, че има списък с числа и колекция от низове . Но Howво ще стане, ако елиминираме параметъра тип и направим следното:

SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Получаваме грешка:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Отново, това е изтриване на типове. Тъй като класът вече не използва параметър тип, компилаторът решава, че тъй като сме предали List , методът с List<Integer> е най-подходящ. И се проваляме с грешка. Следователно имаме Правило #2: Ако имате общ клас, винаги посочвайте параметрите на типа.

Ограничения

Можем да ограничим типовете, посочени в генерични методи и n класове. Да предположим например, че искаме контейнер да приема само число като аргумент тип. Тази функция е описана в раздела Параметри на ограничен тип на урока на Oracle. Да разгледаме един пример:

import java.util.*;
public class HelloWorld {
	
    public static class NumberContainer<T extends Number> {
        private T number;
    
        public NumberContainer(T number) { this.number = number; }
    
        public void print() {
            System.out.println(number);
        }
    }

    public static void main(String []args) {
		NumberContainer number1 = new NumberContainer(2L);
		NumberContainer number2 = new NumberContainer(1);
		NumberContainer number3 = new NumberContainer("f");
    }
}
Както можете да видите, ограничихме параметъра тип до класа/интерфейса Number or неговите наследници. Имайте предвид, че можете да посочите не само клас, но и интерфейси. Например:

public static class NumberContainer<T extends Number & Comparable> {
Генеричните също така поддържат заместващи знаци Те са разделени на три типа: Използването на заместващи знаци трябва да се придържа към принципа Get-Put . Може да се изрази по следния начин:
  • Използвайте заместващ знак за разширение , когато получавате само стойности от структура.
  • Използвайте супер заместващ знак, когато поставяте само стойности в структура.
  • И не използвайте заместващ знак, когато и двамата искате да получите и поставите от/към структура.
Този принцип се нарича още принципът Producer Extends Consumer Super (PECS). Ето малък пример от изходния code за метода Collections.copy на Java : Generics в Java: How да използвате ъглови скоби на практика - 5И ето малък пример за това, което НЯМА да работи:

public static class TestClass {
	public static void print(List<? extends String> list) {
		list.add("Hello, World!");
		System.out.println(list.get(0));
	}
}

public static void main(String []args) {
	List<String> list = new ArrayList<>();
	TestClass.print(list);
}
Но ако замените extends със super , тогава всичко е наред. Тъй като попълваме списъка със стойност, преди да покажем съдържанието му, той е потребител . Съответно използваме супер.

Наследство

Генериците имат друга интересна характеристика: наследяване. Начинът, по който работи наследяването за генерични продукти, е описан в „ Общи форми, наследяване и подтипове “ в урока на Oracle. Важното е да запомните и осъзнаете следното. Не можем да направим това:

List<CharSequence> list1 = new ArrayList<String>();
Тъй като наследяването работи по различен начин с генеричните: Generics в Java: How да използвате ъглови скоби на практика - 6И ето още един добър пример, който ще се провали с грешка:

List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Отново тук всичко е просто. List<String> не е потомък на List<Object> , въпреки че String е потомък на Object . За да затвърдите наученото, ви предлагаме да гледате видео урок от нашия курс по Java

Заключение

Така че освежихме паметта си относно генеричните лекарства. Ако рядко се възползвате напълно от техните възможности, някои от детайлите стават неясни. Надявам се, че този кратък преглед е помогнал за раздвижване на паметта ви. За още по-добри резултати горещо ви препоръчвам да се запознаете със следния материал:
Коментари
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION