Java核心之细说泛型

泛型是什么?

等你使用java逐渐深入以后会了解或逐步使用到Java泛型。Java 中的泛型是 JDK 5 中引入的功能之一。"Java 泛型 "是一个技术术语,表示一组与定义和使用泛型类型和方法有关的语言特性。在 Java 中,泛型类型或方法与普通类型和方法的区别在于它们具有类型参数。

入门

如果仔细观察集合框架类,就会发现大多数类都使用对象类型的参数,并以对象形式从方法中返回值。现在,在这种形式下,它们可以将任何 Java 类型作为参数并返回相同的值。它们本质上是异构的,即不属于特定的相似类型。

像我们这样的程序员经常希望指定一个集合只包含某种类型的元素,例如Integer or String 或 Employee。在最初的集合框架中,如果不在代码中添加额外的检查,就不可能实现同质集合。引入泛型就是为了消除这一限制。它们会在编译时自动在代码中添加这种类型的参数检查。这样,我们就不必编写大量不必要的代码,如果编写得当,这些代码在运行时实际上不会增加任何价值。

泛型通过提供实际的类型参数来替代形式化的类型参数,从而实例化形成参数化的类型。例如下面这样:

public class LinkedList<E> ...
LinkedList<String> list = new LinkedList();
  • 解释
  1. 像 LinkedList 这样的类是一种具有类型参数 E 的泛型。
  2. 像 LinkedList 或 LinkedList 这样的实例被称为参数化类型。
  3. 字符串和整数是各自的实际类型参数。

通俗地说,泛型强制保证了 Java 语言的类型安全。

现在,我们已经对 Java 中为什么会出现泛型有了一定的了解。下一步是了解 Java 中的泛型是如何工作的。在源代码中使用泛型时究竟会发生什么?

泛型是如何工作的?

泛型的核心是 "类型安全"。究竟什么是类型安全?它只是编译器的一种保证,即如果在正确的地方使用了正确的类型,那么在运行时就不会出现任何 ClassCastException

一个用例可以是一个整数列表,即 List。如果您声明了 List 这样的列表,那么 Java 保证会检测并报告在上述列表中插入任何非整数类型的尝试。

List<Integer> list = new ArrayList<>();
list.add(1);
list.add("one");  //compiler error

类型安全

泛型的核心是 "类型安全"。究竟什么是类型安全?它只是编译器的一种保证,即如果在正确的地方使用了正确的类型,那么在运行时就不会出现任何 ClassCastException。

一个用例可以是一个整数列表,即 List。如果您声明了 List 这样的列表,那么 Java 保证会检测并报告在上述列表中插入任何非整数类型的尝试。

List<Integer> list = new ArrayList<>();
list.add(1);
list.add("one");  //compiler error

类型擦除

泛型的另一个重要术语是 "类型擦除"。它的基本意思是,使用泛型添加到源代码中的所有额外信息都将从生成的字节码中删除。在字节码中,如果完全不使用泛型,得到的将是旧的 Java 语法。这必然有助于生成和执行 Java 5 之前编写的代码,因为 Java 5 尚未在语言中添加泛型。
来看一个例子:

List<Integer> list = new ArrayList<>();
list.add(1000);

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

List list = new ArrayList();
list.add(1000);

准确地说,Java 中的 "泛型 "只不过是为了类型安全而给代码添加的语法糖,所有这些类型信息都会被编译器的 "类型清除 "功能抹去。

泛型的分类

现在,我们对通用语有了一些了解。现在开始探索围绕泛型的其他重要概念。首先,我将介绍将属类应用于源代码的各种方法。

类或接口

如果一个类声明了一个或多个类型变量,那么这个类就是泛型。这些类型变量被称为类的类型参数。让我们通过一个例子来理解。
DemoClass 是一个简单的类,它有一个属性 t(也可以多个),属性类型是对象。

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

例如,如果我们希望类的一个实例持有 "字符串 "类型的值 t,那么程序员就应该设置和获取唯一的字符串类型。

由于我们已将属性类型声明为对象,因此无法强制执行这一限制。程序员可以设置任何对象,也可以期望从 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 的使用示例如下:

DemoClass<String> instance = new DemoClass<>();
instance.set("lokesh");   //Correct usage
instance.set(1);        //This will raise compile time error

上述类比同样适用于接口。让我们快速看一个例子,了解接口中如何使用泛型类型信息。

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

我希望我已经说得足够清楚,让大家对泛型类和接口有了一些了解。现在我们来看看泛型方法和构造函数。

泛型方法和构造函数

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

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

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 class MyClass<T> {
    private T value;

    // 泛型构造函数
    public MyClass(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}
MyClass<String> myString = new MyClass<>("Hello");
MyClass<Integer> myInt = new MyClass<>(42);

泛型数组

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

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

与上述通用类型类和方法相同,我们也可以使用通用数组。我们知道,数组是相似类型元素的集合,推送任何不兼容的类型都会在运行时抛出 ArrayStoreException;而集合类则不会出现这种情况。

Object[] array = new String[10];
array[0] = "lokesh";
array[1] = 10;      //This will throw ArrayStoreException

上述错误并不难犯。它随时都可能发生。因此,最好也为数组提供类型信息,以便在编译时就能发现错误。

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

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

泛型通配符

在泛型代码中,问号(?)被称为通配符,代表未知类型。通配符参数化类型是泛型类型的实例化,其中至少有一个类型参数是通配符。通配符参数化类型的例子有 Collection 和 Pair。通配符可以在多种情况下使用:作为参数、字段或局部变量的类型;有时也可以作为返回类型(尽管编程实践中最好更具体一些)。通配符绝对不能用作泛型方法调用、泛型类实例创建或超类型的类型参数。

在不同位置放置通配符也有不同的含义,例如:

Collection 表示 Collection 接口的所有实例,与类型参数无关。
List 表示元素类型为 Number 子类型的所有列表类型。
Comparator<? super String< 表示类型参数类型为 String 的超类型的比较器接口的所有实例。
通配符参数化类型并不是可以出现在新表达式中的具体类型。它只是暗示了泛型执行的规则,即在使用了通配符的任何特定场景中,哪些类型是有效的。

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

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

以下是通配符的无效使用,编译时会出错。

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

泛型中的通配符可以是无界的,也可以是有界的。让我们从不同的术语中找出区别。

无界通配符参数化类型

通用类型,所有类型参数都是无限制通配符"?",对类型变量没有任何限制,例如:

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

有界通配符参数化类型

有界通配符对我们可以用来实例化参数化类型的可能类型施加了一些限制。这种限制通过关键字 "super "和 "extends "来实现。为了更清楚地区分,我们把它们分为上界通配符和下界通配符。

上界通配符

例如,如果您想编写一个适用于 List、List 和 List 的方法,您可以通过使用有上界的通配符来实现,例如,您可以指定 List<? extends Number>。这里,Integer 和 Double 是 Number 类的子类型。通俗地说,如果您想让泛型表达式接受某一特定类型的所有子类,您可以使用关键字 "extends "来使用上界通配符:

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 "关键字的下界通配符来实现这一目的。

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

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

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

哪些行为是不允许的?

到目前为止,我们已经了解了一些使用泛型可以避免在应用程序中出现大量 ClassCastException 实例的方法。我们还了解了通配符的用法。现在,我们要确定一些不允许使用泛型的行为。

静态泛型成员

我们不能在类中定义静态泛型参数化成员。任何这样的尝试都会在编译时产生错误:无法对非静态类型 T 进行静态引用。

public class GenericsExample<T>
{
   private static T member; //This is not allowed
}

不能实例化泛型

任何创建 T 实例的尝试都会失败,并显示错误:无法实例化 T 类型。

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

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

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

final List<int> ids = new ArrayList<>();    //Not allowed
final List<Integer> ids = new ArrayList<>(); //Allowed

我们无法创建泛型异常类

有时,程序员可能需要在抛出异常的同时传递一个泛型类型的实例。这在 Java 中是做不到的。

// causes compiler error
public class GenericException<T> extends Exception {}

当您尝试创建这样一个异常时,您将看到这样一条信息:通用类 GenericException 可能无法子类化 java.lang.Throwable。

关于 Java 泛型的先写到这里,凡事还是需要多实践!

posted @ 2024-03-08 21:57  一锤子技术员  阅读(283)  评论(0编辑  收藏  举报