💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 完整的 Java 泛型教程 > 原文: [https://howtodoinjava.com/java/generics/complete-java-generics-tutorial/](https://howtodoinjava.com/java/generics/complete-java-generics-tutorial/) Java 中的[泛型](https://docs.oracle.com/javase/tutorial/extra/generics/ "generics")作为 JDK 5 的特性之一引入。就个人而言,我发现泛型中使用的尖括号`<>`非常吸引人,它总是使我不得不重新思考我在哪里使用它,或查看它是否以其他人的代码编写。 坦率地说,很长一段时间以来我一直在使用泛型,但我仍然不完全有信心盲目使用它。 在本教程中,我将介绍对 **java 泛型**有用的所有内容以及与它们有关的内容。 如果您认为我可以在教程的任何部分使用更精确的词,或者可以添加示例,或者您不同意我的观点; 给我留言。 我很高兴知道您的观点。 ```java Table of content 1) Why Generics? 2) How Generics works in Java 3) Types of Generics? i) Generic Type Class or Interface ii) Generic Type Method or Constructor 4) Generic Type Arrays 5) Generics with Wildcards i) Unbounded Wildcards ii) Bounded Wildcards a) Upper Bounded Wildcards b) Lower Bounded Wildcards 6) What is not allowed to do with Generics? ``` “**Java 泛型**”是一个技术术语,表示与泛型类型和方法的定义和使用有关的一组语言特性。 在 Java 中,泛型类型或方法与常规类型和方法的不同之处在于它们具有类型参数。 > “Java 泛型是一种语言特性,允许定义和使用泛型类型和方法。” 通过提供替代正式类型参数的实际类型参数,实例化泛型类型以形成参数化类型。 像`LinkedList<E>`这样的类是泛型,其类型参数为 E。 诸如`LinkedList<Integer>`或`LinkedList<String>`之类的实例称为参数化类型,而`String`和`Integer`是各自的实际类型参数。 ## 1)为什么要泛型? 如果仔细观察 [**Java 集合框架**](//howtodoinjava.com/java/collections/useful-java-collection-interview-questions/ "Useful java collection interview questions")类,则您会发现大多数类都采用`Object`类型的参数/参数,并从方法中返回`Object`作为值。 现在,以这种形式,他们可以将任何 Java 类型用作参数并返回相同的值。 它们本质上是异构的,即不是特定的相似类型。 像我们这样的程序员经常想指定一个集合只包含某种类型的元素,例如 `Integer`或`String`或`Employee`。 在原始的集合框架中,如果在代码中添加一些检查之前没有添加额外的检查,就不可能拥有同类收集。 引入泛型来消除此限制是非常具体的。 他们会在编译时自动在代码中添加这种类型的参数检查。 这可以节省我们编写大量不必要的代码的时间,如果编写正确的话,这些代码实际上不会在运行时添加任何值。 > “用通俗易懂的术语来说,泛型使用 Java 语言强制类型安全。” 没有这种类型的安全性,您的代码可能会感染各种错误,而这些错误只会在运行时才被发现。 使用泛型,使它们在编译时本身突出显示,甚至在获得 Java 源代码文件的字节码之前,也使代码健壮。 > “泛型通过在编译时检测到更多错误来增加代码的稳定性。” 因此,现在我们有了一个合理的想法,为什么泛型首先出现在 java 中。 下一步是了解有关它们如何在 Java 中工作的知识。 在源代码中使用泛型时实际发生的情况。 ## 2)泛型在 Java 中的工作方式 泛型的核心是“[**类型安全性**](https://en.wikipedia.org/wiki/Type_safety "type safety")”。 类型安全到底是什么? 编译器只是保证,如果在正确的位置使用正确的类型,则运行时不应有任何`ClassCastException`。 用例可以是`Integer`的列表,即`List<Integer>`。 如果您在 Java 中声明`List<Integer>`之类的列表,则 Java 保证它将检测并报告您将任何非整数类型插入上述列表的任何尝试。 Java 泛型中的另一个重要术语是“[**类型擦除**](https://en.wikipedia.org/wiki/Type_erasure "type erasure")”。 从本质上讲,这意味着使用泛型添加到源代码中的所有额外信息将从其生成的字节码中删除。 在字节码内部,它将是旧的 Java 语法,如果您根本不使用泛型,则会得到该语法。 当未在语言中添加泛型时,这必然有助于生成和执行在 Java 5 之前编写的代码。 让我们看一个例子。 ```java List<Integer> list = new ArrayList<Integer>(); list.add(1000); //works fine list.add("lokesh"); //compile time error; ``` 当您编写上述代码并进行编译时,会出现以下错误:“类型为`List<Integer>`的`add(Integer)`方法不适用于参数`(String)`”。 编译器警告您。 这正是泛型的唯一目的,即类型安全。 第二部分是从上述示例中删除第二行后获得字节码。 如果将上面的示例的字节码与泛型进行比较,则没有任何区别。 显然,编译器删除了所有泛型信息。 因此,上面的代码与没有泛型的下面的代码非常相似。 ```java List list = new ArrayList(); list.add(1000); ``` > “准确地说,Java 中的泛型不过是类型安全代码的语法糖,所有此类类型信息都将由编译器的类型擦除特性擦除。” ## 3)泛型的类型? 现在我们对泛型的意义有了一些了解。 现在开始探索围绕泛型的其他重要概念。 我将从确定各种方法开始,泛型可以应用于源代码。 #### 泛型类或接口 如果一个类声明一个或多个类型变量,则它是泛型的。 这些类型变量称为类的类型参数。 让我们看一个例子。 `DemoClass`是简单的 Java 类,具有一个属性`t`(也可以不止一个); 属性的类型是`Object`。 ```java class DemoClass { private Object t; public void set(Object t) { this.t = t; } public Object get() { return t; } } ``` 在这里,我们希望一旦使用某种类型初始化了该类,则该类应仅与该特定类型一起使用。 例如如果我们要让一个类的实例保存类型为“`String`”的值`t`,那么程序员应该设置并获取唯一的`String`类型。 由于我们已将属性类型声明为`Object`,因此无法强制执行此限制。 程序员可以设置任何对象。 由于所有 Java 类型都是`Object`类的子类型,因此可以从`get`方法获得任何返回值类型。 要强制执行此类型限制,我们可以使用以下泛型: ```java class DemoClass<T> { //T stands for "Type" private T t; public void set(T t) { this.t = t; } public T get() { return t; } } ``` 现在我们可以放心,不会将类误用于错误的类型。 `DemoClass`的用法示例如下所示: ```java DemoClass<String> instance = new DemoClass<String>(); instance.set("lokesh"); //Correct usage instance.set(1); //This will raise compile time error ``` 上面的类比也适用于接口。 让我们快速看一个示例,以了解如何在 Java 接口中使用泛型类型信息。 ```java //Generic interface definition interface DemoInterface<T1, T2> { T2 doSomeOperation(T1 t); T1 doReverseOperation(T2 t); } //A class implementing generic interface class DemoClass implements DemoInterface<String, Integer> { public Integer doSomeOperation(String t) { //some code } public String doReverseOperation(Integer t) { //some code } } ``` 我希望我足够清楚地介绍泛型类和接口。 现在该看一下泛型方法和构造器了。 #### 泛型方法或构造器 泛型方法与泛型类非常相似。 它们仅在一个方面不同,即类型信息的范围仅在方法(或构造器)内部。 泛型方法是引入自己的类型参数的方法。 让我们通过一个例子来理解这一点。 下面是一种泛型方法的代码示例,该方法可用于在该类型的变量列表中查找所有出现的类型参数。 ```java public static <T> int countAllOccurrences(T[] list, T item) { int count = 0; if (item == null) { for ( T listItem : list ) if (listItem == null) count++; } else { for ( T listItem : list ) if (item.equals(listItem)) count++; } return count; } ``` 如果在此方法中传递`String`的列表和另一个字符串进行搜索,它将可以正常工作。 但是,如果尝试在`String`列表中找到`Number`,则会出现编译时错误。 与上述相同可以作为泛型构造器的示例。 让我们也为泛型构造器提供一个单独的示例。 ```java class Dimension<T> { private T length; private T width; private T height; //Generic constructor public Dimension(T length, T width, T height) { super(); this.length = length; this.width = width; this.height = height; } } ``` 在此示例中,`Dimension`类的构造器也具有类型信息。 因此,您可以仅具有单一类型的所有属性的维度实例。 ## 4)泛型数组 任何语言中的数组都具有相同的含义,即数组是元素类型相似的集合。 在 Java 中,在运行时将任何不兼容的类型推入数组将抛出`ArrayStoreException`。 这意味着数组在运行时保留其类型信息,而泛型使用类型擦除或在运行时删除任何类型信息。 由于上述冲突,不允许在 Java 中实例化泛型数组。 ```java public class GenericArray<T> { // this one is fine public T[] notYetInstantiatedArray; // causes compiler error; Cannot create a generic array of T public T[] array = new T[5]; } ``` 与上述泛型类型类和方法相同,我们可以在 java 中拥有泛型数组。 如我们所知,数组是元素类型相似的集合,并且推送任何不兼容的类型都会在运行时抛出`ArrayStoreException`; `Collection`类不是这种情况。 ```java Object[] array = new String[10]; array[0] = "lokesh"; array[1] = 10; //This will throw ArrayStoreException ``` 犯上述错误并不是很难。 它可以随时发生。 因此,最好也将类型信息提供给数组,以便在编译时就捕获错误。 数组不支持泛型的另一个原因是数组是协变的,这意味着超类型引用的数组是子类型引用的数组的超类型。 也就是说,`Object[]`是`String[]`的超类型,并且可以通过类型`Object[]`的引用变量访问字符串数组。 ```java Object[] objArr = new String[10]; // fine objArr[0] = new String(); ``` ## 5)带有通配符的泛型 在泛型代码中,称为通配符的问号(`?`)表示未知类型。 **通配符参数化类型是泛型类型的实例,其中至少一个类型参数是通配符。** 通配符参数化类型的示例是`Collection<?>`,`List<? extends Number>`,`Comparator<? super String>`和`Pair<String, ?>`。 通配符可以在多种情况下使用:作为参数,字段或局部变量的类型; 有时作为返回类型(尽管更具体的做法是更好的编程习惯)。 通配符从不用作泛型方法调用,泛型类实例创建或超类型的类型参数。 在不同的地方使用通配符也具有不同的含义。 例如 * `Collection<?>`表示`Collection`接口的所有实例化,与类型参数无关。 * `List<? extends Number>`表示所有列表类型,其中元素类型是`Number`的子类型。 * `Comparator<? super String>`表示`Comparator`接口的所有实例化,这些实例化类型是`String`的超类型。 通配符参数化类型不是可能出现在新表达式中的具体类型。 它只是暗示了 Java 泛型所执行的规则,即在使用通配符的任何特定情况下,哪种类型均有效。 例如,以下是涉及通配符的有效声明: ```java Collection<?> coll = new ArrayList<String>(); //OR List<? extends Number> list = new ArrayList<Long>(); //OR Pair<String,?> pair = new Pair<String,Integer>(); ``` 以下是通配符的无效用法,它们将给出编译时错误。 ```java List<? extends Number> list = new ArrayList<String>(); //String is not subclass of Number; so error //OR Comparator<? super String> cmp = new RuleBasedCollator(new Integer(100)); //Integer is not superclass of String ``` 泛型中的通配符可以是无界的,也可以是有界的。 让我们找出不同方面的差异。 #### 无界通配符参数化类型 一种泛型类型,其中所有类型参数都是无界通配符`?`,对类型变量没有任何限制。 例如 ```java ArrayList<?> list = new ArrayList<Long>(); //or ArrayList<?> list = new ArrayList<String>(); //or ArrayList<?> list = new ArrayList<Employee>(); ``` #### 有界通配符参数化类型 有界通配符对可能的类型施加了一些限制,您可以用来实例化参数化类型。 使用关键字`super`和`extends`强制执行此限制。 为了更清楚地区分,我们将它们分为上限通配符和下限通配符。 ##### 上限通配符 例如,假设您要编写一种适用于`List<String>`,`List<Integer>`和`List<Double>`的方法,则可以使用上限通配符来实现,例如您将指定`List<? extends Number>`。 这里的`Integer`,`Double`是`Number`类的子类型。 用通俗易懂的话来说,如果您希望泛型表达式接受特定类型的所有子类,则可以使用`extends`关键字来使用上限通配符。 ```java public class GenericsExample<T> { public static void main(String[] args) { //List of Integers List<Integer> ints = Arrays.asList(1,2,3,4,5); System.out.println(sum(ints)); //List of Doubles List<Double> doubles = Arrays.asList(1.5d,2d,3d); System.out.println(sum(doubles)); List<String> strings = Arrays.asList("1","2"); //This will give compilation error as :: The method sum(List<? extends Number>) in the //type GenericsExample<T> is not applicable for the arguments (List<String>) System.out.println(sum(strings)); } //Method will accept private static Number sum (List<? extends Number> numbers){ double s = 0.0; for (Number n : numbers) s += n.doubleValue(); return s; } } ``` ##### 下界通配符 如果您希望泛型表达式接受所有类型,即特定类型的“超”类型或特定类的父类,则可以使用`super`关键字使用下界通配符。 在下面给出的示例中,我创建了三个类,即`SuperClass`,`ChildClass`和`GrandChildClass`。 下面的代码显示了这种关系。 现在,我们必须创建一个以某种方式(例如从 DB)获取`GrandChildClass`信息并创建其实例的方法。 我们希望将此新的`GrandChildClass`存储在`GrandChildClasses`的现有列表中。 这里的问题是`GrandChildClass`是`ChildClass`和`SuperClass`的子类型。 因此,任何`SuperClasses`和`ChildClasses`的泛型列表都可以容纳`GrandChildClasses`。 在这里,我们必须使用`super`关键字来使用下界通配符。 ```java package test.core; import java.util.ArrayList; import java.util.List; public class GenericsExample<T> { public static void main(String[] args) { //List of grand children List<GrandChildClass> grandChildren = new ArrayList<GrandChildClass>(); grandChildren.add(new GrandChildClass()); addGrandChildren(grandChildren); //List of grand childs List<ChildClass> childs = new ArrayList<ChildClass>(); childs.add(new GrandChildClass()); addGrandChildren(childs); //List of grand supers List<SuperClass> supers = new ArrayList<SuperClass>(); supers.add(new GrandChildClass()); addGrandChildren(supers); } public static void addGrandChildren(List<? super GrandChildClass> grandChildren) { grandChildren.add(new GrandChildClass()); System.out.println(grandChildren); } } class SuperClass{ } class ChildClass extends SuperClass{ } class GrandChildClass extends ChildClass{ } ``` ## 6)不允许使用泛型? 到目前为止,我们已经了解了许多可使用 Java 中的泛型来避免应用程序中许多`ClassCastException`实例的方法。 我们还看到了通配符的用法。 现在是时候确定一些 Java 泛型中不允许执行的任务。 #### a)您不能使用类型的静态字段 您不能在类中定义静态的泛型参数化成员。 这样做的任何尝试都会产生编译时错误:无法静态引用非静态类型`T`。 ```java public class GenericsExample<T> { private static T member; //This is not allowed } ``` #### b)您不能创建`T`的实例 任何创建`T`实例的尝试都会失败,并显示以下错误:无法实例化类型`T`。 ```java public class GenericsExample<T> { public GenericsExample(){ new T(); } } ``` #### c)泛型与声明中的原始类型不兼容 对,是真的。 您不能声明泛型表达式,例如`List`或`Map <String,double>`。 绝对可以使用包装器类代替原始类型,然后在传递实际值时使用原始类型。 通过使用自动装箱将原始类型转换为相应的包装器类,可以接受这些值原始类型。 ```java final List<int> ids = new ArrayList<>(); //Not allowed final List<Integer> ids = new ArrayList<>(); //Allowed ``` #### d)您无法创建泛型异常类 有时,程序员可能需要传递泛型类型的实例以及引发异常。 在 Java 中这是不可能的。 ```java // causes compiler error public class GenericException<T> extends Exception {} ``` 当您尝试创建这样的异常时,您将得到如下消息:泛型类`GenericException`可能不是子类`java.lang.Throwable`。 到此为止,这一切都结束了,这次是关于 **java 泛型**的讨论。 在以后的文章中,我将提出与泛型相关的更多有趣的事实和特性。 如果有不清楚的地方,请给我评论/或者您还有其他问题。 学习愉快!