Java泛型详解教程

后端 潘老师 7个月前 (10-16) 143 ℃ (0) 扫码查看

Java中的泛型是JDK 5中引入的功能之一。”Java Generics”是一个技术术语,表示与泛型类型和方法的定义和使用相关的一组语言特性。在Java中,泛型类型或方法与常规类型和方法不同,因为它们具有类型参数。

“Java泛型是一种语言特性,允许定义和使用泛型类型和方法。”

泛型类型通过提供实际类型参数来实例化,以形成参数化类型,替换了形式上的类型参数。

public class LinkedList<E> ...

LinkedList<String> list = new LinkedList();
  • 像LinkedList<E>这样的类是具有类型参数E的泛型类型。
  • 像LinkedList<Integer>或LinkedList<String>这样的实例化称为参数化类型。
  • String和Integer是相应的实际类型参数。

1.泛型简介

如果仔细查看集合框架类,您会发现大多数类以Object类型的参数并将方法的返回值作为Object返回。现在,在这种形式中,它们可以接受任何Java类型作为参数并返回相同的类型。它们本质上是异构的,即不属于特定相似类型。

像我们这样的程序员经常希望指定集合仅包含某种类型的元素,例如Integer、String或Employee。在原始的集合框架中,如果不在代码中添加额外的检查,将不可能拥有同质的集合。泛型被引入以消除这种限制,使其更具体。它在编译时自动在您的代码中添加此类型的参数检查。如果正确编写,它将使我们免于编写很多不必要的代码,这些代码实际上在运行时没有增加任何价值。

“简而言之,泛型强制Java语言中的类型安全。”

如果没有这种类型的安全性,您的代码可能会受到各种只在运行时才能显现的错误的影响。使用泛型可以使这些错误在编译时就被突出显示,使我们的代码在获得Java源代码文件的字节码之前就更加健壮。

“泛型通过使更多的错误在编译时可检测来为您的代码增加稳定性。”

现在我们已经大致了解了为什么Java首次引入了泛型。下一步是了解在Java中使用泛型时发生了什么。当您在源代码中使用泛型时,实际发生了什么呢?

2. 泛型如何工作?

2.1. 类型安全

泛型的核心概念是“类型安全”。什么是类型安全?它只是编译器的保证,如果在正确的位置使用了正确的类型,那么在运行时不应该出现ClassCastException。

一个用例可以是整数列表,即List<Integer>。如果声明一个列表如List<Integer>,那么Java保证它将检测并报告任何尝试将任何非整数类型插入上述列表的操作。

List<Integer> list = new ArrayList<>();
list.add(1);
list.add("one");  //编译报错

2.2. 类型擦除

泛型中的另一个重要术语是“类型擦除”。它实际上意味着使用泛型在源代码中添加的所有附加信息将从生成的字节码中删除。在字节码中,它将是旧的Java语法,如果不使用泛型,你将获得它。这有助于生成和执行在Java 5之前没有添加泛型到语言中时编写的代码。

让我们通过一个示例来理解。

List<Integer> list = new ArrayList<>();

list.add(1000);

如果将上述示例的使用泛型和与下述代码不使用泛型编译后的字节码进行比较,那么不会有任何区别。显然,编译器删除了所有泛型信息。因此,上述代码与不使用泛型的以下代码非常相似。

List list = new ArrayList();

list.add(1000);
“准确地说,Java中的泛型只是为了类型安全而添加的一种语法糖,而编译器通过类型擦除功能删除了所有这些类型信息。”

3.泛型类型

现在我们对泛型的基本概念有一些了解。现在开始探索围绕泛型的其他重要概念。我将从识别可以应用于源代码的各种方式开始使用泛型。

3.1. 类或接口

如果一个类声明了一个或多个类型变量,那么它是泛型类。这些类型变量被称为类的类型参数。让我们通过一个示例来了解。

DemoClass是一个简单的类,它有一个属性t(也可以是多个属性);属性的类型是Object。

class DemoClass {
   private Object t;
   public void set(Object t) { this.t = t; }
   public Object get() { return t; }
}

在这里,我们希望一旦使用特定类型初始化类,就应该只使用该特定类型的类。例如,如果我们希望类的一个实例持有类型为“String”的值,那么程序员应该设置并获取只有String类型。

由于我们将属性类型声明为Object,因此没有办法强制执行此限制。程序员可以设置任何对象,并期望从get()方法获得任何返回值类型,因为所有Java类型都是Object类的子类型。

为了强制执行这种类型的限制,我们可以使用泛型,如下所示:

class DemoClass<T> {
   //T 代表 "Type"
   private T t;
   public void set(T t) { this.t = t; }
   public T get() { return t; }
}

现在我们可以确保类不会被错误使用。DemoClass的示例用法将如下所示:

DemoClass<String> instance = new DemoClass<>();
 // 正确的用法,将字符串类型的值设置到DemoClass实例
instance.set("lokesh"); 
 // 这将在编译时引发错误,因为DemoClass<String>已经指定为String类型,无法将整数设置为该实例
instance.set(1);       

上述类似的类也适用于接口。让我们快速看一个示例,以了解泛型类型信息如何在接口中使用。

//泛型接口定义
interface DemoInterface<T1, T2>
{
   T2 doSomeOperation(T1 t);
   T1 doReverseOperation(T2 t);
}
//一个类实现泛型接口
class DemoClass implements DemoInterface<String, Integer>
{
   public Integer doSomeOperation(String t)
   {
      //some code
   }
   public String doReverseOperation(Integer t)
   {
      //some code
   }
}

希望我对泛型类和接口的一些特性解释得足够清楚,现在是时候看一下泛型方法和构造函数。

3.2. 方法或构造函数

泛型方法与泛型类非常相似。它们只有一个方面不同,类型信息的范围仅在方法(或构造函数)内部。泛型方法是引入自己的类型参数的方法。

让我们通过一个示例来了解。下面是一个泛型方法的代码示例,可以用于查找该类型参数的所有变量列表中的所有出现。

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;
}

如果将字符串列表和另一个要在此方法中搜索的字符串传递给该方法,它将正常工作。但如果尝试在字符串列表中查找Number,它将在编译时出错。

与上述相同,泛型构造函数也可以作为示例。让我们为泛型构造函数单独举一个例子。

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;
}

在此示例中,Dimension类的构造函数也具有类型信息。因此,您只能拥有一个所有属性都是相同类型的维度实例。

4.泛型数组

任何语言中的数组都具有相同的含义,即数组是相似类型元素的集合。在Java中,将不兼容类型的元素推送到数组中会引发ArrayStoreException。这意味着数组在运行时保留其类型信息,而泛型使用类型擦除或在运行时删除任何类型信息。由于上述冲突,不允许实例化泛型数组。

public class GenericArray<T> {
    // 这是可以的,只是一个泛型数组引用,但没有实际实例化
    public T[] notYetInstantiatedArray;
    // 这会引发编译错误; 不能创建一个泛型数组 T
    public T[] array = new T[5]; 
}

与上述泛型类型类和方法相同,我们可以有泛型数组。正如我们知道,数组是相似类型元素的集合,并在运行时将任何不兼容类型推送到数组中会引发ArrayStoreException;而与集合类不同的是,泛型使用类型信息来接受这些元素。

Object[] array = new String[10];
array[0] = "lokesh";
array[1] = 10;      //这将抛出ArrayStoreException

上述错误不太容易发生。它可以随时发生。因此,最好也为数组提供类型信息,以便在编译时捕获错误。

数组不支持泛型的另一个原因是数组是协变的,这意味着超类型引用的数组是子类型引用数组的超类型。也就是说,Object[]是String[]的超类型,字符串数组可以通过类型为Object[]的引用变量访问。

Object[] objArr = new String[10];  // 正常
objArr[0] = new String();

5.通配符的泛型

在泛型代码中,问号(?)称为通配符,表示未知类型。通配符参数化类型是泛型类型的实例,其中至少一个类型参数是通配符。通配符参数化类型的示例包括Collection<?>, List<? extends Number>, Comparator<? super String>和Pair<String, ?>。通配符可以在各种情况下使用:作为参数、字段或局部变量的类型;有时作为返回类型(虽然更好的编程实践是更具体)。通配符永远不用作泛型方法调用、泛型类实例创建或超类型的类型参数。

在不同位置使用通配符具有不同的含义。例如:

  • Collection表示Collection接口的所有实例化,而不管类型参数。
  • List表示所有元素类型为Number的列表类型。
  • Comparator<? super String>表示String的超类型的类型参数类型的Comparator接口的所有实例化。

通配符参数化类型不是可以出现在新表达式中的具体类型。它只提示了泛型强制执行的规则,表明在使用通配符的情况下哪些类型在特定情况下是有效的。

例如,以下是涉及通配符的有效声明:

Collection<?> coll = new ArrayList<String>();
//或
List<? extends Number> list = new ArrayList<Long>();
//或
Pair<String,?> pair = new Pair<String,Integer>();

以下是无效使用通配符的示例,它们会导致编译时错误。

 //String不是Number子类,所以报错
List<? extends Number> list = new ArrayList<String>(); 
//或
 //Integer 不是String子类,所以报错
Comparator<? super String> cmp = new RuleBasedCollator(new Integer(100));

泛型中的通配符可以是无限的,也可以是有限的。让我们分辨各种术语的不同之处。

5.1. 无界通配符参数化类型

一个通用类型,其中所有类型参数都是没有任何限制的通配符“?”。例如:

ArrayList<?>  list = new ArrayList<Long>();
//或
ArrayList<?>  list = new ArrayList<String>();
//或
ArrayList<?>  list = new ArrayList<Employee>();

5.2. 有界通配符参数化类型

有界通配符对可以用于实例化参数化类型的可能类型施加了一些限制。使用“super”和“extends”关键字来强制执行此限制。为了更清晰地区分它们,让我们将它们分为上界通配符和下界通配符。

5.3. 上界通配符

例如,假设您想编写一个可以在List<String>、List<Integer>和List<double>上工作的方法,您可以使用上界通配符,例如,您可以指定List<? extends Number>。在这里,整数和Double是Number类的子类型。简而言之,如果要使通用表达式接受特定类型的所有子类,将使用“extends”关键字的上限通配符。

public class GenericsExample<T> {
   public static void main(String[] args) {
      // 整数列表
      List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
      System.out.println(sum(ints));
      // 双精度浮点数列表
      List<Double> doubles = Arrays.asList(1.5d, 2d, 3d);
      System.out.println(sum(doubles));
      List<String> strings = Arrays.asList("1", "2");
      // 这会导致编译错误,因为sum方法的参数是List<? extends Number>,而不是List<String>
      System.out.println(sum(strings));
   }

   // 方法接受 Number 或其子类的列表
   private static Number sum(List<? extends Number> numbers) {
      double s = 0.0;
      for (Number n : numbers)
         s += n.doubleValue();
      return s;
   }
}

5.4. 下界通配符

如果要使通用表达式接受所有“super”类型或特定类的父类的所有类型,那么可以使用下界通配符,使用“super”关键字。

在下面的示例中,我创建了三个类,即SuperClass、ChildClass和GrandChildClass。它们之间的关系在下面的代码中显示。现在,我们必须创建一个方法,该方法以某种方式获取GrandChildClass信息(例如,从数据库中获取),并创建GrandChildClass的实例。我们希望将这个新的GrandChildClass存储在已经存在的GrandChildClasses列表中。

在这里的问题是GrandChildClass是ChildClass和SuperClass的子类型。因此,任何SuperClasses和ChildClasses的通用列表都可以容纳GrandChildClasses。在这里,我们必须使用“super”关键字的下界通配符来帮助。

public class GenericsExample<T> {
   public static void main(String[] args) {
      // 包含GrandChildClass的列表
      List<GrandChildClass> grandChildren = new ArrayList<GrandChildClass>();
      grandChildren.add(new GrandChildClass());
      addGrandChildren(grandChildren);

      // 包含ChildClass的列表
      List<ChildClass> childs = new ArrayList<ChildClass>();
      childs.add(new GrandChildClass());
      addGrandChildren(childs);

      // 包含SuperClass的列表
      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.泛型中不允许的内容

到目前为止,我们已经学习了如何使用泛型来避免应用程序中的许多ClassCastException实例。我们还看到了通配符的使用。现在是时候识别在泛型中不允许执行的一些任务。

6.1. 我们不能拥有类型的静态字段

我们不能在类中定义一个静态的泛型参数化成员。任何尝试这样做的尝试都将生成编译时错误:“Cannot make a static reference to the non-static type T.”

public class GenericsExample<T>
{
   private static T member; //不允许
}

6.2. 我们不能创建T的实例

尝试创建T的实例将失败,并显示错误:“Cannot instantiate the type T.”

public class GenericsExample<T>
{
   public GenericsExample(){
      new T();
   }
}

6.3. 泛型与声明中的原始类型不兼容

是的,这是真的。您不能声明像List或Map<String, double>这样的通用表达式。当传递实际值时,可以使用包装类来替代基本类型,然后使用基本类型。这些基本类型的值是通过自动装箱将基本类型转换为相应的包装类来接受的。

final List<int> ids = new ArrayList<>();    //不允许
final List<Integer> ids = new ArrayList<>(); //允许

6.4. 我们不能创建通用异常类

有时,程序员可能需要在引发异常时传递通用类型的实例。

//编译报错
public class GenericException<T> extends Exception {}

在Java中无法执行此操作。尝试创建这样的异常将导致类似于“GenericException类不能子类化java.lang.Throwable”的消息。

到此为止,我在本次关于Java泛型的讨论中介绍这些内容。如果有什么不清楚的地方,或者如果您有其他问题,请给我留下评论。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/9702.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】