iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > Python >Java泛型Generic机制实例详解
  • 503
分享到

Java泛型Generic机制实例详解

Java泛型GenericJavaGeneric 2022-11-13 19:11:39 503人浏览 泡泡鱼

Python 官方文档:入门教程 => 点击学习

摘要

目录泛型是什么?泛型类泛型方法类型变量的限制类型擦除通配符上界通配符下界通配符无界通配符反射和泛型类型字面量限制和局限性不能使用基本类型实例化类型参数运行时类型查询只适用于原始类型不

泛型是什么?

使用泛型可以指定类型变量,从而让代码可以对不同类型的对象进行重用。以及,还可以让编译器更好的了解类型,从而避免强制类型转换,提升代码的安全性。

类型变量就是尖括号 <>中的变量,类型变量的命名规范是使用大写字母,例如 E 表示元素类型,K、V 分别表示键和值类型,T 和相邻的 U、S 表示任意类型。当然你也可以起其他的名字,编译器对此并没有强制限制,但是还是按照规范来。

泛型类

泛型类(generic class)就是有一个或多个类型变量的类。例如下面的 Pair 类,就有两个类型变量,分别是 T 和 U:

public class Pair<T, U> {
  public T first;
  public U second;
  public Pair(T first, U second) {
    this.first = first;
    this.second = second;
  }
}

在使用时,就可以传入任意的类型。例如:

Pair<Integer, String> pair = new Pair<>(1, "A");

思考一下,如果没有泛型的存在,那么当我们要操作不同类型的对象时,要么为每种类型都创建一个类,但是这样就会存在大量的重复代码,不容易维护。要么就直接使用对象的顶级父类 Object 类,但是这样的话在使用时就需要进行强制转换,存在安全问题,即使每次强转前进行判断,也会存在重复代码和遗漏的风险。

说回泛型类,泛型类的类型变量是定义在类名后面,在整个类中都可以使用定义的类型变量。所以在选择使用泛型类时,泛型变量应该是和类所关联的。如果仅与方法关联,那么可以使用泛型方法。

泛型方法

泛型方法的类型参数是定义在方法返回类型的前面的,只在当前方法中可以使用。例如:

public static <T> T getMiddle(T... a) {
  return a[a.length / 2];
}

泛型方法可以在普通类中定义,也可以在泛型类中定义。如果泛型变量仅在方法内会用到,就可以考虑使用泛型方法。

类型变量的限制

默认情况下,类型变量可以是任何类型。但是有时候,我们需要限制类型变量的类型。此时可以通过 extends关键字来限制:

  • <T extends UpperBoundType> 限制泛型类型为特定类型或者特定类型的子类

例如,假设我们有一个 Printer 类,我们只需要这个类打印动物的信息,那么就可以使用 <T extends Animal> 来限制只能接收 Animal 或其实现类:

public interface Animal {
  String getName();
}
public static class Printer<T extends Animal> {
  public void print(T t) {
    System.out.println(t.getName());
  }
}
public static class Dog implements Animal {
  @Override
  public String getName() {
    return "Woof";
  }
}
public static class Cat implements Animal {
  @Override
  public String getName() {
    return "Meow";
  }
}
public static void main(String[] args) {
  Printer<Animal> animalPrinter = new Printer<>();
  animalPrinter.print(new Dog());
  animalPrinter.print(new Cat());
  animalPrinter.print(new People("Abc")); // 如果试图传入一个其他类型,则会编译失败
}

一个类型变量,可以指定多个限制类型。例如:

public class Printer<T extends Dog & Animal> {
  ...
}

上面的限制的意思是,类型变量必须是 Dog 或其子类,并且实现了 Animal 接口。需要注意的是,在指定多个限制的类型时,除了第一个限制类型可以是类以外,其他的限制类型必须是接口类型。这是因为 Java 是不支持多重继承的。

类型擦除

由于泛型是在 Java 1.5 才推出的,所以 Java 为了保证在之前版本上的兼容性,泛型实际上在 JVM 中是没有的!

也就是说,编译器在编译时会擦除掉泛型的参数类型,并提供一个相应的原始类型(raw type),这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除(erased),并替换为其限定类型,没有限定类型则替换为 Object 类型。

举个例子,如果我们定义了一个泛型类:

public class Printer<T> {
  ...
}

那么当类型擦除后,实际上就变为了 Printer<Object> 类型。

如果指定了限定类型,例如:

public class Printer<T extends Animal> {
  ...
}

那么擦除后,就变为了 Printer<Animal> 类型。

如果指定了多个限定的类型,那么擦除后就会是第一个限定的类型。例如:

public class Printer<T extends Dog & Animal> {
  ...
}

擦除后就变为了 Printer<Dog>类型。

Java 泛型的很多限制都是由于泛型擦除导致的,下面会详细介绍下泛型的局限性。

通配符

通配符(Wildcard)就是 Java 泛型中的问号 ? 。要想知道通配符是要解决什么样的问题,我觉得首先需要知道什么是 Variance(不知道这个正确的译名是什么,如有知道请留言)?

提到 Variance 必须先提到子类型(subtyping),子类型是面向对象中类型多态的其中一种表现形式,主要用于描述 is-a 这样的关系,例如 S 是 T 的子类型,那么 S is a subtype of T。

Variance 指的是如何根据组成类型之间的子类型关系,来确定更复杂的类型之间的子类型关系。

上面这段话比较绕,举个实际的例子来说,Variance 指的就是当子类型在更复杂的场景下,例如 If S is a subtype of T,那 Generic<S> is subtype of Generic<T> 这种关系是否还能成立。

Variance 分为下面几种形式:

  • 不变(Invariance):如果 B 是 A 的子类型,那么 Generic<B> 不是 Generic<A> 的子类型。
  • 协变(Covariance):如果 B 是 A 的子类型,那么 Generic<B>Generic<A> 的子类型。
  • 逆变(Contravariance):如果 B 是 A 的子类型,那么 Generic<A>Generic<B> 的子类型(逆变就是逆转了子类型关系)。
  • 双变(Bi-variance):Java 中不存在双变,因此不讨论。

在 Java 当中,泛型是不变的,这也就意味着下面这段看似没问题的代码会报错:

List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // 编译报错

因为按照不变的说明,List<Dog> 根本就不是 List<Animal> 的子类型,所以自然就不能赋值了。

Java 中的泛型采用不变性是为了保证类型安全。不过不变性虽然保证了类型安全,但是也让灵活性大大降低。为此,Java 提供了通配符,可以解除部分这种限制,也不损失安全性。

上界通配符

上界通配符的形式为:? extends Type,使用上限通配符可以将类型参数变为协变的,这就可以让原本无法赋值的类型可以正常赋值了。但是这在解除不变性限制的同时,也带来了新的限制,就是参数类型只能作为返回值类型,无法作为入参的类型。例如:

List<? extends Animal> animals = new ArrayList<Dog>(); // 可以正确赋值了
animals.add(new Dog()); // 编译错误,类型不匹配,无法写入,除非写入 null(也就是参数类型无法作为入参的类型)
Animal animal = animals.get(0); // 可以获取(也就是参数类型可作为返回值类型)

这就叫当上帝给你关了一扇门,同时会帮你打开一扇窗。尽管窗户有个洞,但总比没有好吧?你说是不是?

下界通配符

下界通配符的形式为:? super Type,使用下界通配符可以将类型参数变为逆变的,也就是说颠倒了类型关系,父类型可以赋值给子类型了!例如:

List<Animal> animals = new ArrayList<>();
List<? super Dog> dogs = animals; // 可以正确赋值!

看起来挺神奇的吧!这种方式同样也带来了新的限制,在使用这种变量的时候,不可以调用返回值包含类型参数的方法,也不能获得包含类型参数的字段值。简单来说,就是只能作为输入,但是不能输出。例如:

dogs.add(new Dog()); // 可以写入
Dog dog = dogs.get(0); // 编译报错,类型不匹配: Required type: Dog  Provided: capture of ? super Dog

对于获取时编译报类型不匹配的问题,其实可以通过类型强制转换来解决。

无界通配符

无界通配符的形式就是一个单独的问号 ?。当我们对类型参数一无所知,但仍然希望以安全的方式使用它时,则可以考虑使用无界通配符。例如:

public static boolean hasNull(Pair<?> p) {
  return p.first != null && p.second != null;
}

反射和泛型

因为泛型擦除,所以无法在反射时获取泛型类型。但是擦除的类依然保留原先泛型的微弱记忆。例如,如果定义了以下泛型方法:

public static <T extends Comparable<? super T>> T min(T[] a)

那么,可以通过反射可以获取到以下信息:

public static Comparable min(Comparable[] a)

也就是说,我们可以重新构造实现者声明的泛型类和方法的有关内容。但是,不会知道对于特定的对象或方法调用会如何解析类型参数。

为了表述泛型类型声明,可以使用 java.lang.reflect包中的接口 Type。这个接口包含以下子类型:

  • Class 类:描述具体类型
  • TypeVariable 接口:描述类型变量(如 T extends Comparable<? super T>
  • WildcardType 接口:描述通配符(如 ? super T
  • ParameterizedType 接口:描述泛型类或接口类型(如 Comparable<? super T>
  • GenericArrrayType 接口:描述泛型数组(如 T[]

下面的这个示例程序,就是通过反射来打印指定类的信息:

public class GenericReflectionTest {
  public static void main(String[] args) {
    String className;
    try (Scanner scanner = new Scanner(System.in)) {
      System.out.println("Enter class name (e.g., java.util.Collections): ");
      className = scanner.next();
    }
    try {
      Class<?> cl = Class.forName(className);
      ClassPrinter.print(cl);
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }
  }
  public static class ClassPrinter {
    public static void print(Class<?> cl) {
      System.out.print(cl);
      printTypes(cl.getTypeParameters(), "<", ", ", ">", true);
      final Type sc = cl.getGenericSuperclass();
      if (sc != null) {
        System.out.print(" extends ");
        printType(sc, false);
      }
      printTypes(cl.getGenericInterfaces(), " implements ", ", ", "", false);
      System.out.print(" {");
      System.out.println();
      final Method[] methods = cl.getDeclaredMethods();
      for (int i = 0; i < methods.length; i++) {
        final Method method = methods[i];
        printMethod(method);
        if (i < methods.length - 1) {
          System.out.println();
        }
      }
      System.out.println();
      System.out.print("}");
    }
    private static void printTypes(Type[] types, String pre, String sep, String suf, boolean isDefinition) {
      if (pre.equals(" extends ") && Arrays.equals(types, new Type[]{Object.class})) {
        return;
      }
      if (types.length > 0) {
        System.out.print(pre);
      }
      for (int i = 0; i < types.length; i++) {
        if (i > 0) {
          System.out.print(sep);
        }
        printType(types[i], isDefinition);
      }
      if (types.length > 0) {
        System.out.print(suf);
      }
    }
    private static void printType(Type type, boolean isDefinition) {
      if (type instanceof Class) { // 具体类型
        Class<?> cl = (Class<?>) type;
        System.out.print(cl.getName());
      } else if (type instanceof TypeVariable) { // 类型变量
        TypeVariable<?> tv = (TypeVariable<?>) type;
        System.out.print(tv.getName());
        if (isDefinition) {
          printTypes(tv.getBounds(), " extends ", " & ", "", false);
        }
      } else if (type instanceof WildcardType) { // 通配符
        WildcardType wt = (WildcardType) type;
        System.out.print("?");
        printTypes(wt.getUpperBounds(), " extends ", " & ", "", false);
        printTypes(wt.getLowerBounds(), " super ", " & ", "", false);
      } else if (type instanceof ParameterizedType) { // 泛型类或接口类型
        ParameterizedType pt = (ParameterizedType) type;
        Type ownerType = pt.getOwnerType();
        if (ownerType != null) {
          printType(ownerType, false);
        }
        printType(pt.getRawType(), false);
        printTypes(pt.getActualTypeArguments(), "<", ", ", ">", false);
      } else if (type instanceof GenericArrayType) { // 泛型数组
        GenericArrayType gat = (GenericArrayType) type;
        printType(gat.getGenericComponentType(), isDefinition);
        System.out.print("[]");
      } else {
        System.out.print("unknown type: " + type);
      }
    }
    private static void printMethod(Method m) {
      final String name = m.getName();
      System.out.print("  ");
      System.out.print(Modifier.toString(m.getModifiers()));
      System.out.print(" ");
      printTypes(m.getTypeParameters(), "<", ", ", ">", true);
      printType(m.getGenericReturnType(), false);
      System.out.print(" ");
      System.out.print(name);
      System.out.print("(");
      printTypes(m.getGenericParameterTypes(), "", ", ", "", false);
      System.out.print(")");
    }
  }
}

以下面的这个测试类为例:

public class TestClass<T extends Comparable<? super T>> implements Cloneable {
  public void test(T t) {
    System.out.println(t);
  }
}

运行上面的程序的输出结果是:

class com.airsaid.java.samples.generic.TestClass<T extends java.lang.Comparable<? super T>> extends java.lang.Object implements java.lang.Cloneable {
  public void test(T)
}

可以看到,可以获取到该泛型类所定义的泛型信息。

类型字面量

有时候,我们确实需要在运行时获取泛型的参数类型,那么真的就没有办法了吗?

答案是有的,在上面反射的例子中我们可以看到,字节码当中其实保存了部分的泛型信息(签名信息)。

举一个实际场景的例子,Gson 这个框架相信大家都用过。在进行反序列化时,有多个重载方法:

fromJSON(String json, Class cl)

fromJson(String json, TypeToken type)

当使用第一个方法时,如果第二个参数是一个泛型类的 class 对象,那么就会出现问题。例如下面的这段示例代码:

Foo<DataBean> foo = new Foo<DataBean>();
gson.fromJson(json, foo.getClass());

这是因为无法获取到 Foo 类的泛型参数 DataBean。

因此,Gson 提供了 TypeToken 类来处理这种问题。使用起来是这样的:

Foo<DataBean> foo = new Foo<DataBean>();
gson.fromJson(json, new TypeToken<Foo<DataBean>>(){});

下面是我简化过后的 TypeToken 源码

public class TypeToken<T> {
  private final Type type;
  protected TypeToken() {
    this.type = getTypeTokenTypeArgument();
  }
  public Type getType() {
    return type;
  }
  private Type getTypeTokenTypeArgument() {
    final Type superclass = getClass().getGenericSuperclass();
    if (superclass instanceof ParameterizedType) {
      ParameterizedType parameterized = (ParameterizedType) superclass;
      if (parameterized.getRawType() == TypeToken.class) {
        return parameterized.getActualTypeArguments()[0];
      }
    } else if (superclass == TypeToken.class) {
      throw new IllegalStateException("TypeToken must be created with a type argument: new TypeToken<...>() {}; "
                                      + "When using code shrinkers (ProGuard, R8, ...) make sure that generic signatures are preserved.");
    }
    throw new IllegalStateException("Must only create direct subclasses of TypeToken");
  }
}

TypeToken 的原理就是创建了一个匿名子类对象,然后通过 getGenericSuperclass()方法获取其泛型父类,再获取其中定义的参数化类型的类型参数。通过这种方式,就可以获取到 TypeToken 所定义的泛型类型了:

public class TypeTokenTest {
  public static void main(String[] args) {
    TypeToken<?> typeToken = new TypeToken<List<String>>() {};
    System.out.println(typeToken.getType()); // java.util.List<java.lang.String>
  }
}

限制和局限性

不能使用基本类型实例化类型参数

泛型的类型参数不可以使用基本类型,例如 Pair<int>,这是因为类型擦除后类型变为了 Object,而 Object 无法存储基本类型数值。对于这种情况,我们需要使用基本类型对应的包装类型。

运行时类型查询只适用于原始类型

同样也是由于类型擦除导致的问题,如果要在代码中进行判断是否是某个泛型类型,这是无法做到的。例如:

if (a instanceof Pair<String>) // 错误

不能创建参数化类型的数组

还是由于类型擦除导致的问题,无法实例化参数化类型的数组。例如:

Pair<String>[] table = new Pair<String>[10]; // 错误

不能创建 Throwable 的子类为泛型类

如果希望创建一个 Throwable 的子类,那么子类就不可以是泛型类。例如:

class Generic<T> extends Exception {
}

此时,编译器会直接报错,提示:Subclass of 'Throwable' may not have type parameters

这还是因为泛型擦除,这就导致在 catch 时不知道该执行哪个代码分支:

try {
    ...
} catch (Generic<String> e) {
} catch (Generic<Integer> e) {
}

以上就是Java 泛型 Generic机制实例详解的详细内容,更多关于Java 泛型 Generic的资料请关注编程网其它相关文章!

--结束END--

本文标题: Java泛型Generic机制实例详解

本文链接: https://www.lsjlt.com/news/171042.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • Java泛型Generic机制实例详解
    目录泛型是什么?泛型类泛型方法类型变量的限制类型擦除通配符上界通配符下界通配符无界通配符反射和泛型类型字面量限制和局限性不能使用基本类型实例化类型参数运行时类型查询只适用于原始类型不...
    99+
    2022-11-13
    Java 泛型 Generic Java Generic
  • 深入理解java泛型Generic
    目录一、背景二、泛型概念三、泛型类3.1 定义与调用3.2 注意3.3 使用3.4 泛型类的继承3.4.1 子类也是泛型类3.4.2 子类不是泛型类四、泛型接口4.1 定义4.2 使...
    99+
    2024-04-02
  • java 泛型的详解及实例
    java 泛型的详解及实例Java在1.5版本中增加了泛型,在没有泛型之前,从集合中读取每一个对象都需要进行强转,如果一不小心插入了类型错误的对象,在运行时就会报错,给日常开发带来了很多不必要的麻烦,比如以下代码:public class ...
    99+
    2023-05-31
    java 泛型 ava
  • Java中泛型使用实例详解
    Java中泛型使用泛型作用:泛型:集合类添加对象不用强转反射机制:将泛型固定的类的所有方法和成员全部显示出来 核心代码:ArrayList<Ls> ff=new ArrayList()<Ls>;Ls ls1...
    99+
    2023-05-31
    java 泛型 ava
  • Java 获取泛型的类型实例详解
    Java 获取泛型的类型实例详解Java 泛型实际上有很多缺陷,比如不能直接获取泛型的类型,不能获取带泛型类等。以下方式是不正确的:①.获取带泛型的类的类型Class lstUClazz = List<User>.class...
    99+
    2023-05-31
    java 泛型 类型
  • Java泛型之类型擦除实例详解
    目录前言泛型是什么?泛型的定义和使用泛型类泛型方法泛型类与泛型方法的共存现象泛型接口通配符?无限定通配符<><extendsT>类型擦除类型擦除带来的局限性泛...
    99+
    2024-04-02
  • Java 泛型详解与范例
    目录一、泛型的使用二、泛型类的定义-类型边界三、类型擦除四、泛型类的使用-通配符五、泛型方法六、泛型的限制一、泛型的使用 前面我们学集合的时候,简单的说过泛型的使用。如下: Ar...
    99+
    2024-04-02
  • Java中泛型的示例详解
    目录泛型概述使用泛型的好处泛型的定义与使用定义和使用含有泛型的类含有泛型的方法含有泛型的接口泛型通配符通配符基本使用通配符高级使用----受限泛型泛型概述 我们都知道集合中是可以存放...
    99+
    2022-11-13
    Java泛型机制 Java泛型
  • Java泛型的类型擦除示例详解
    目录前言泛型的类型擦除原则是:1 擦除类定义中的类型参数1.1 无限制类型擦除1.2 有限制类型擦除2 擦除方法定义中的类型参数3 桥接方法和泛型的多态总结参考资料前言 Java泛型...
    99+
    2024-04-02
  • Java 泛型详解(超详细的java泛型方法解析)
    目录2. 什么是泛型3. 使用泛型的好处4. 泛型的使用4.1 泛型类4.2 泛型方法4.3 泛型接口5. 泛型通配符5.1 通配符基本使用5.2 通配符高级使用6. 总结1. 为什...
    99+
    2024-04-02
  • Kotlin 泛型详解及简单实例
     Kotlin 泛型详解概述一般类和函数,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的约束对代码的限制很大。而OOP的多态采用了一种泛化的机制,在SE 5种,Java引用了泛...
    99+
    2023-05-31
    kotlin 泛型
  • Java泛型实例分析
    这篇文章主要介绍“Java泛型实例分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Java泛型实例分析”文章能帮助大家解决问题。首先,我们来看看泛型Java的经典用法:import java.ut...
    99+
    2023-06-03
  • 详解Java中的泛型
    目录一.什么是泛型二.泛型类的使用2.1泛型类的定义2.2泛型类的数组使用三.泛型的上界四.泛型的方法五.泛型与集合一.什么是泛型 当我们不确定数据类型时,我们可以暂时使用一个字母 ...
    99+
    2023-05-19
    Java泛型 Java泛型类
  • java回调机制实例详解
    java回调机制实例详解以前不理解什么叫回调,天天听人家说加一个回调方法啥的,心里想我草,什么叫回调方法啊?然后自己就在网上找啊找啊找,找了很多也不是很明白,现在知道了,所谓回调:就是A类中调用B类中的某个方法C,然后B类中反过来调用A类中...
    99+
    2023-05-31
    java 回调机制 ava
  • Java 反射机制详解及实例
    Java 反射机制详解及实例反射,当时经常听他们说,自己也看过一些资料,也可能在设计模式中使用过,但是感觉对它没有一个较深入的了解,这次重新学习了一下,感觉还行吧!      &n...
    99+
    2023-05-31
    java 反射机制 ava
  • Java 反射机制的实例详解
    Java 反射机制的实例详解前言 今天介绍下Java的反射机制,以前我们获取一个类的实例都是使用new一个实例出来。那样太low了,今天跟我一起来学习学习一种更加高大上的方式来实现。正文 Java反射机制定义Java反射机制是指在运行状态中...
    99+
    2023-05-31
    java 反射机制 ava
  • java 代理机制的实例详解
    java 代理机制的实例详解前言: java代理分静态代理和动态代理,动态代理有jdk代理和cglib代理两种,在运行时生成新的子类class文件。本文主要练习下动态代理,代码用于备忘。对于代理的原理和机制,网上有很多写的很好的,...
    99+
    2023-05-31
    java 代理机制 ava
  • Java中泛型机制有什么用
    小编给大家分享一下Java中泛型机制有什么用,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!Java 在 5.0 时增加了泛型机制,据说专家们为此花费了 5 年左右...
    99+
    2023-06-02
  • TypeScrip中泛型的案例详解
    泛型的定义 // 需求一: 泛型 可以支持不特定的数据类型, 要求,传入的参数和返回参数一致 // 这种方式虽然能实现传入和返回的参数一致,但是失去类型参数检验 // 定义...
    99+
    2024-04-02
  • Spring实现泛型注入的示例详解
    目录1.Spring泛型注入2. 关于java泛型有四种TypeGenericArrayType泛型数组类型ParameterizedType参数化类型TypeVariable 类型...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作