Java 泛型知识总结

1、基础知识

泛型是 JDK5 引入的参数化类型特性,所谓参数化类型就是把类型当成参数一样传递,它使得一个类型在定义类、接口和方法时成为一个参数,类似于方法声明中的形式参数,不同之处在于形式参数的输入是值,而类型形参的输入是类型。

为什么要使用泛型

与非泛型代码相比,使用泛型的代码具有许多优点:

  • 使代码更健壮。Java 编译器会对泛型代码进行强类型检查,如果代码违反类型安全,则会编译报错。修复编译时错误比修复运行时错误容易,后者可能很难找到。将运行时潜在的类型转换异常提前到编译时检查,也是泛型的重要作用之一。只要编译期没有警告,运行期就不会出现 ClassCastException

  • 使代码更简洁。泛型消除了强制类型转换,非泛型代码段需要强制转换:

    List list = new ArrayList();
    list.add("hello");
    String s = (String) list.get(0);
    

    使用泛型重写时,代码不需要强制转换:

    List<String> list = new ArrayList<String>();
    list.add("hello");
    String s = list.get(0); // no cast
    
  • 使代码更灵活。通过使用泛型,程序员可以实现泛型算法,这些算法可以处理不同类型的集合(增强了代码复用性),可以自定义,并且类型安全且易于阅读

泛型相关术语

  • Plate<T> 中的 T 被称为类型参数,而 Plate<T> 整个被称为泛型类型,Plate 则称为原始类型
  • Plate<Banana> 中的 Banana 被称为实际类型参数,而 Plate<Banana> 整个被称为参数化的类型(Parameterized Type)

基础使用

泛型可以定义在类、接口和方法上:

// 泛型接口
public interface Box<T> {public void set(T t);public T get();
}// 泛型类,不知道 BoxImpl 具体的泛型类型
public BoxImpl<T> implements Box<T> {public void set(T t);public T get();
}// 泛型类,知道 BoxImpl 具体的泛型类型是 Apple
public BoxImpl implements Plate<Apple> {public void set(Apple apple);public Apple get();
}// 泛型方法,泛型方法上的类型参数范围仅限于声明它的方法,
// 方法上的类型参数列表要位于返回类型之前
public <T> BoxImpl<T> getBox() {return new BoxImpl<>();
}

需要注意类上声明的泛型只对非静态成员有效,但是泛型方法可以是一个静态方法。

此外官方对于泛型的类型形参有命名规范:

  • E - Element (Java 集合框架广泛使用)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

可以对类型参数进行限定,限定主要有两个作用:

  1. 限制可用于实例化泛型类型的类型
  2. 限定类型参数允许你调用在范围中定义的方法

比如说:

public class Box<T> {private T t;public void set(T t) {this.t = t;}public T get() {return t;}public <U extends Number> void inspect(U u){System.out.println("T: " + t.getClass().getName());System.out.println("U: " + u.getClass().getName());}public static void main(String[] args) {Box<Integer> integerBox = new Box<Integer>();integerBox.set(new Integer(10));// 参数 U 必须继承自 Number,输入字符串会报错,体现第 1 点integerBox.inspect("some text"); // error: this is still String!}
}
--------------------------------------------------------------------------------
public class NaturalNumber<T extends Integer> {private T n;public NaturalNumber(T n) { this.n = n; }public boolean isEven() {// n 的类型被限定为继承 Integer,所以它可以使用 Integer 的方法,体现第 2 点return n.intValue() % 2 == 0;}// ...
}

如果一个类型参数具有多个限定:

<T extends B1 & B2 & B3>

且 B1、B2、B3 中有一个是类的话,那么它必须居于首位,否则会编译报错。此外,为了提高效率,应该将标签接口(即没有方法的接口)放在边界列表的末尾,如 Serializable。比如说一个泛型类 Data<T extends Serializable & Comparable>,发生类型擦除至原始类型后,会用 Serializable 替换 T,使得编译器在必要时会向 Comparable 插入强制类型转换。

类型推断

类型推断是 Java 编译器查看每个方法调用和相应声明,以使用适用的类型参数的能力。推断算法确定参数的类型,以及确定结果是否被分配或返回的类型(如果有)。最后,推断算法尝试找到与所有参数一起使用的最具体的类型。比如说,在下面的示例中,会推断传递给 pick 方法的第二个参数的类型为 Serializable:

static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());

泛型方法也引入了类型推断,可以像调用普通方法一样调用泛型方法,而无需在尖括号之间指定类型。

从 Java SE 7 开始,只要编译器可以从上下文确定或推断出类型参数,就可以用一组空的类型参数( <> )替换调用泛型类的构造函数所需的类型参数:

	// Java7 开始 <> 内的类型在可以类型推断的情况下可以省略Box<Integer> integerBox = new Box<>();// Java7 之前调用构造方法时 <> 内的类型不能省略Box<Integer> integerBox = new Box<Integer>();

2、通配符与泛型限定

实际使用泛型时,可能会出现这种情形:编写一种适用于 List<Number>List<Integer>List<Double> 的方法。总不能以每一种类型作为形参写出三个方法吧?如果想要用一个方法解决,这时就需要用到泛型限定了。

泛型限定依赖于通配符 ?,<? extends A> 是泛型上限,表示该类型是 A 本身或其子类型,<? super A> 是泛型下限,表示该类型是 A 本身或其父类,而 <?> 则为不受限的通配符。

泛型上下限

上面说到的编写适用于 List<Number>List<Integer>List<Double> 的方法,就可以用泛型上限来实现。比如说要计算 List 元素中所有数据之和:

    private double addToSum(List<? extends Number> list) {double sum = 0.0;for (Number number : list) {sum += number.doubleValue();}return sum;}

使用 List<? extends Number> 类型作为形参可以接收 List<Number>List<Integer>List<Double> 等多种类型。这就体现出泛型上限的优点,即扩大了兼容的范围,使得方法形参的匹配范围从单一的 List<Number> 扩展到泛型类型为 Number 本身及其子类的 List。

但是这样做也有两个缺点:

  1. 写:不能再向 List<? extends Number> 中添加新元素或修改已经存在的元素了。但是添加 null,或者通过反射添加/修改元素是可以的,只是没法保证类型安全,可能会出现运行时类型转换异常。
  2. 读:从 List<? extends Number> 中取元素时,该元素只能被转成 Object 或者上限类型 Number。原因是编译器只知道容器内装着 Number 或者它的派生类对象,但是并不知道被取出元素的具体类型,可能是 Integer,也可能是 Float 或 Double。所以在使用限定泛型 List<? extends Number> 之后,它的类型标签不是某个具体的类型,而是一个占位符 CAP#1,来表示捕获一个 Number 或 Number 的子类,具体是什么类型,不知道……这也解释了上一条中为什么不能向使用了上限的集合中添加元素的原因,因为编译器不知道你要添加的元素类型是否与占位符 CAP#1 匹配,所以干脆就都不允许添加了。

List<? extends XXX> 通常被非正式的认为是只读的,比如:

class NaturalNumber {private int i;public NaturalNumber(int i) { this.i = i; }// ...
}
class EvenNumber extends NaturalNumber {public EvenNumber(int i) { super(i); }// ...
}

你可以将 List<EvenNumber> 类型的对象赋值给 List<? extends NaturalNumber>,但是不能调用后者的 add() 添加元素:

List<EvenNumber> le = new ArrayList<>();
List<? extends NaturalNumber> ln = le;
ln.add(new NaturalNumber(35)); // compile-time error

因为 List<EvenNumber>List<? extends NaturalNumber> 的一个子类型,所以可以将 le 赋给 ln,但是不能用 ln 将自然数添加到偶数列表中,它可以对该列表做以下操作:

  • 可以添加 null
  • 可以调用 clear
  • 可以获取迭代器( iterator )和调用 remove
  • 可以捕获通配符和写入从列表中读取的元素

所以 List<? extends Xxx> 并不是严格意义上的只读,它是不能在列表中存储一个新的元素或改变一个现有的元素。

类似地,泛型下限也是有副作用的:

	public void test() {Box<? super Integer> box = new Box<Integer>();// 可以box.set(new Integer());// 可以box.set(new Number());// 不可以,编译报错,box 可以存数据,但是取数据时泛型信息丢失了,只能用 Object 存放Integer num1 = box.get();// 可以,泛型下限取出的数据只能存成 Object 类型Object num2 = box.get(); }

无界通配符

出现在泛型上限如 List<? extends XXX> 和泛型下限如 List<? super XXX> 中的 ? 分别被称为上界通配符和下界通配符。而单个的 ? 如 List<?> 被称为无界通配符,该泛型类等价于 List<? extends Object>

使用无界通配符的泛型既不能读也不能写(只能插入 null),它用来进行类型安全检查:

  • List 不会进行类型安全检查。
  • List<?> 会进行类型安全检查。

有两种情况,无界通配符是一种有用的方法:

  • 如果你正在编写可以使用 Object 类中提供的功能来实现的方法。
  • 当代码使用泛型类中不依赖于类型形参的方法时。例如,List.size() 或 List.clear()。实际上,Class<?> 经常被使用,因为 Class<T> 中的大多数方法都不依赖于 T。

比如说你想编写方法 printList 打印来打印任何类型的列表:

    public static void printList(List<Object> list) {for (Object elem : list)System.out.println(elem + " ");System.out.println();}

如果像上面那样使用 List<Object> 作为形参类型就无法实现这个目标,因为它只能接收 List<Object>,无法接收其他类型的列表,因为它们不是 List<Object> 的子类型。只有把 List<Object> 改为 List<?> 才可以,因为对于任何具体类型 T,List<T> 都是 List<?> 的子类型:

通配符使用准则

为了便于讨论,将变量视为提供以下两个功能之一将很有帮助:

  • "输入"变量:输入变量将数据提供给代码。想象一个具有两个参数的复制方法: copy(src, dest) 。src 参数提供要复制的数据,因此它是输入参数。
  • "输出"变量:输出变量保存要在其他地方使用的数据。在复制示例 copy(src, dest) 中,dest 参数接受数据,因此它是输出参数。

当然,某些变量既用于“输入”又用于“输出”目的(准则中也解决了这种情况)。准则如下:

  • 使用上限通配符定义输入变量,使用 extends 关键字。
  • 使用下限通配符定义输出变量,使用 super 关键字。
  • 如果可以使用 Object 类中定义的方法访问输入变量,请使用无界通配符( ? )。
  • 如果代码需要同时使用输入和输出变量来访问变量,则不要使用通配符。

这些准则不适用于方法的返回类型。应该避免使用通配符作为返回类型,因为这会迫使程序员使用代码来处理通配符。

与上述观点类似的是泛型的 PECS 原则:Producer Extends Consumer Super,站在 List 的角度,从 List 中取元素的是生产者 Producer,只读不写时应该用 Extends 限定;而向 List 中写入元素的是消费者 Consumer,写入时应该用 Super 限定。典型用例是 Collections 中的 copy 方法:

    public static <T> void copy(List<? super T> dest, List<? extends T> src) {int srcSize = src.size();if (srcSize > dest.size()) {throw new IndexOutOfBoundsException("Source does not fit in dest");} else {if (srcSize < 10 || src instanceof RandomAccess && dest instanceof RandomAccess) {for (int i = 0; i < srcSize; ++i) {dest.set(i, src.get(i));}} else {ListIterator di = dest.listIterator();ListIterator si = src.listIterator();for (int i = 0; i < srcSize; ++i) {di.next();di.set(si.next());}}}}

通配符捕获

Collections 中的 swap() 能体现出 List<?> 的作用:

    public static void swap(@NotNull List<?> list, int i, int j) {list.set(i, list.set(j, list.get(i)));}

Collections 的源码就是这么写的,由于前边说到过 List<?> 这种无限定通配符既不能读也不能写,因此我们自己在写 list.get(i) 时其实是编译报错的:

不兼容的类型: Object无法转换为CAP#1var0.set(var1, var0.set(var2, var0.get(var1)));^其中, CAP#1是新类型变量:CAP#1从?的捕获扩展Object

根据官方文档指引,可以通过编写捕获通配符的私有帮助器来修复它:

    public static void swap(List<?> list, int i, int j) {swapHelper(list, i, j);}public static <T> void swapHelper(List<T> list, int i, int j) {list.set(i, list.set(j, list.get(i)));}

这种方法其实是通过参数传递把 List<?> 传递给了 swapHelper() 的参数 List<T>,做了一步类型推断从而避免了编译错误。

各种泛型形式的区分

最后我们对泛型中的各种形式进行区分:

  • Plate 原始类型,不做类型安全检查
  • Plate<Object> 可读可写,可存任何类型数据
  • Plate<?> 不可读写(只能插入 null),唯一作用是做类型检查,相当于 Plate<? extends Object>
  • Plate<T> 泛型类型,可读可写
  • Plate<? extends T> 泛型上限,可读不可写(但可以写入 null 或者通过反射写入任何类型的值)
  • Plate<? super T> 泛型下限,可写入运行时类型及其子类类型元素,作为方法参数赋值时可以指向 T 及其父类型

3、泛型中的赋值问题

泛型的继承和子类型

只要类型兼容就可以将一种类型的的对象分配给另一种类型的对象。例如,你可以将一个 Integer 分配给一个 Object ,因为 Object 是 Integer 的超类型之一:

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK

由于 Integer “is a” Object,所以允许这样分配。泛型也是如此,你可以执行泛型类型调用,将 Number 作为其类型参数传递,并且如果该参数与 Number 兼容,则可以随后进行 add 的任何后续调用:

Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK

但是有一种情况要注意:

public void boxTest(Box<Number> n) { /* ... */ }

此时你不能在调用该方法时传入一个 Box<Integer> 作为参数,因为 Integer 继承自 Number,但是 Box<Integer>Box<Number> 之间没有继承关系,它们的共同父类是 Object。类似的,List<Number>List<Integer> 之间也没有继承关系,二者的公共父类是 List<?>。再延伸一步,由于 List<? extends Integer>List<? extends Number> 的子类型,所以前者可以赋值给后者:

记住判断两个对象之间能否赋值,就看这两个对象的类型是否兼容,对于泛型类和非泛型类都是如此。

在泛型中,除了上图中表示的限定类型相关的继承关系之外,还有一种继承关系,假设有关系 ColorPlate -> BigPlate -> Plate(-> 表示继承),那么对于同一个参数类型 T,以上继承关系仍然成立,即 ColorPlate<T> -> BigPlate<T> -> Plate<T>。

原始类型

原始类型(Raw Types)是没有任何类型参数的泛型类或接口的名称。例如,给定通用 Box 类:

public class Box<T> {public void set(T t) { /* ... */ }// ...
}

正常创建 Box 的参数化类型需要为 T 指定一个实际的类型参数,如

Box<Integer> intBox = new Box<>();

但如果省略类型参数,创建的就是 Box 的原始类型:

Box rawBox = new Box();

因此 Box 是通用类型 Box<T> 的原始类型,但是需要注意非泛型类或接口不是原始类型(类上没有类型参数的不算原始类型)。JDK5 之前,诸如 Collections 类的许多 API 类不是通用的,使用原始类型时实际上会获得泛型行为(指 Box 为你提供对象)。为了向后兼容,允许将参数化类型分配给其原始类型:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;

但是反之则会收到警告,该警告表明原始类型会绕过通用类型检查,从而将不安全代码的捕获推迟到运行时。因此,应避免使用原始类型:

Box rawBox = new Box(); // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox; // warning: unchecked conversion

因为你不知道 rawBox 中的元素是否都是 Integer 类型的……类似的,如果你使用原始类型来调用在相应的泛型类型中定义的泛型方法,也会收到警告:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8); // warning: unchecked invocation to set(T)

总之,无论是将原始类型给泛型类型,还是将泛型类型给原始类型,都会破坏类型安全,有发生运行时异常的可能:

	// 情况一:public static void testGeneric() {List rawList = new ArrayList();rawList.add(1);// Unchecked assignment: 'java.util.List' to 'java.util.List<java.lang.String>'List<String> stringList = rawList; // 这是有问题的,但是编译没报错String string = stringList.get(0); // 这里类型转换的时候抛异常了}// 情况二:public static void testGeneric() {List<String> stringList = new ArrayList<>();stringList.add("1");List rawList = stringList;Object object = rawList.get(0); // 转换成 Object 可以Integer integer = rawList.get(0); // 转换成其他类型编译时就会报错}

上面示例代码的情况一会引发堆污染。堆污染发生于泛型类型的变量引用的是不属于该泛型类型的对象时。如果程序执行某些操作,在编译时产生未经检查的警告,就会出现这种情况。如果在编译时(在编译时类型检查规则的限制内)或在运行时,涉及参数化类型的操作(例如,类型转换或方法调用)的正确性无法验证时,则会生成 unchecked warning (未经检查的警告)。例如,在混合原始类型和参数化类型时,或者在执行未经检查的强制转换时,会发生堆污染。

在正常情况下,当所有代码同时编译时,编译器会生成未经检查的警告,以引起你对潜在堆污染的注意。如果单独编译代码的各个部分,则很难检测到堆污染的潜在风险。如果确保代码在没有警告的情况下编译,则不会发生堆污染。

如果产生了 unchecked warning,可以使用 @SuppressWarnings(“unchecked”) 注解抑制该警告。

4、类型擦除

Java 的泛型是一种“伪泛型”,它是在编译期实现的,并不会在虚拟机层面产生新的类。比如 List<Number>.getClass() == List<Integer>.getClass() 这个表达式的值为 true,二者得到的都是 List.class。Java 编译器是通过泛型擦除实现这一点的。

泛型擦除的具体内容:

  1. 若泛型为无限定的类型,如 T、E,则替换为 Object
  2. 若泛型为有限定的类型,如 <T extends A & B & C>,则替换为第一个限定类型,在这里就是 A
  3. 必要时插入类型转换,以保持类型安全
  4. 生成桥接方法以在扩展的泛型类型中保留多态

类型擦除可确保不会为参数化类型创建新的类,产生的字节码仅包含普通的类,接口和方法。因此,泛型不会产生运行时开销。

考虑泛型的时候,不能以运行时的思维去想,虚拟机中没有泛型,只在编译阶段,编译器才考虑泛型。

比如说泛型无限定时:

public class Plate<T> {private T data;
}

data 的类型直接被擦除为 Object:

public class com/frank/generic/Plate {// compiled from: Plate.javaprivate Ljava/lang/Object; data// ...
}

而当泛型有限定时:

public class Plate<T extends Comparable<T> & Serializable> {private T data;
}

会被替换为第一种限定类型 Comparable:

public class com/frank/generic/Plate {// compiled from: Plate.java// access flags 0x2// signature TT;// declaration: Tprivate Ljava/lang/Comparable; data// ...
}

当限定中存在多种类型时,如果既有类又有接口,那么类一定要放在接口之前

桥方法

以上的擦除处理会在多态时产生问题,为了解决这个问题,编译器使用了桥方法。

一个简单的场景是,需要实现一个带有泛型的接口,其源代码和字节码如下:

public interface Play<T> {void set(T t);
}// Play 对应的字节码
public abstract interface com/frank/generic/Play {public abstract set(Ljava/lang/Object;)V
}

实现类 Person 的源码和字节码:

public class Person implements Play<String> {private String data;@Overridepublic void set(String data) {this.data = data;}
}// 字节码
public class com/frank/generic/Person implements com/frank/generic/Play  {// compiled from: Person.java// access flags 0x2private Ljava/lang/String; data// access flags 0x1public <init>()VL0LINENUMBER 3 L0ALOAD 0INVOKESPECIAL java/lang/Object.<init> ()VRETURNL1LOCALVARIABLE this Lcom/frank/generic/Person; L0 L1 0MAXSTACK = 1MAXLOCALS = 1// access flags 0x1public set(Ljava/lang/String;)VL0LINENUMBER 9 L0ALOAD 0ALOAD 1PUTFIELD com/frank/generic/Person.data : Ljava/lang/String;L1LINENUMBER 10 L1RETURNL2LOCALVARIABLE this Lcom/frank/generic/Person; L0 L2 0LOCALVARIABLE data Ljava/lang/String; L0 L2 1MAXSTACK = 2MAXLOCALS = 2// 编译器生成的桥方法// access flags 0x1041public synthetic bridge set(Ljava/lang/Object;)VL0LINENUMBER 3 L0ALOAD 0ALOAD 1CHECKCAST java/lang/StringINVOKEVIRTUAL com/frank/generic/Person.set (Ljava/lang/String;)VRETURNL1LOCALVARIABLE this Lcom/frank/generic/Person; L0 L1 0MAXSTACK = 2MAXLOCALS = 2
}

Play 接口中的 set() 中的泛型被擦除为 Object,而 Person 中的 set() 的参数类型仍为 String,也就是说泛型擦除后,Person 并没有实现 Play 中的 set() 方法。因此编译器自动生成了一个桥方法 public synthetic bridge set(Ljava/lang/Object;),以此实现 Play 接口中的 set():

  // Play 中的 set() 擦除后:void set(Object object);// Person 中的 set():public void set(String data) {this.data = data;}// Person 中的桥方法:public void set(Object object) {set((String)object);}

泛型与反射

既然泛型在编译阶段被擦除,字节码中没有泛型信息,那为什么反射能拿到相关信息呢?

public class Test {private Map<String, Integer> map = new HashMap<>();public static void main(String[] args) throws NoSuchFieldException {Field field = Test.class.getDeclaredField("map");System.out.println(field.getGenericType()); // java.util.Map<java.lang.String, java.lang.Integer>if (field.getGenericType() instanceof ParameterizedType) {ParameterizedType genericType = (ParameterizedType) field.getGenericType();System.out.println(genericType.getRawType()); // interface java.util.Mapfor (Type type : genericType.getActualTypeArguments()) {System.out.println(type); // class java.lang.String,class java.lang.Integer}System.out.println(genericType.getOwnerType()); // null}}
}

这是因为泛型信息虽然被擦除不在字节码中,但是被保存在类的常量池里。Retrofit 正是利用了这一点才实现了通过 Call<XXX> 生成一个 XXX 实例的。

为了表达泛型类型声明,使用 java.lang.reflect 包中提供的接口 Type,该接口包含了一个实现类 Class 和四个实现接口,他们分别是:

  • TypeVariable 泛型类型变量。可以获取泛型上下限等信息,如 T extends Comparable\<? super T>
  • ParameterizedType 具体的泛型类型,可以获得元数据中泛型签名类型(泛型真实类型),如 Comparable<? super T>
  • GenericArrayType 当需要描述的类型是泛型类的数组时,比如 T[](List[]、Map[]),此接口会作为 Type 的实现
  • WildcardType 通配符泛型,获得上下限信息,如 ? super T

泛型的约束与局限性

这些约束与局限性大多是因为类型擦除造成的。

泛型类型变量不能使用基本类型

比如没有 ArrayList<int>,只有 ArrayList<Integer>。这是因为当类型擦除后,ArrayList 的原始类中的类型变量 T 被替换成 Object,但 Object 类型不能存放 int 值。

不能使用 instanceof 运算符

因为擦除后,ArrayList<String> 只剩下原始类型,泛型信息 String 不存在了,所有没法使用 instanceof:

ArrayList<String> strings = new ArrayList<>();
// 不可以,因为 String 会被擦除掉
if (strings instanceof ArrayList<String>)
// 可以
if (strings instanceof ArrayList<?>)

不能使用泛型修饰静态成员

泛型类中的类型参数的实例化是在定义泛型类型对象(比如 ArrayList<Integer>)的时候指定的,而静态成员是不需要使用对象来调用的,所有对象都没创建,如何确定这个泛型参数是什么。因此,如下形式是不可以使用的:

class Test<T> {// 类型参数不可以修饰静态变量public static T data;// 类型参数不可以修饰静态方法public static T test(T t) {...}// 可以,这是泛型方法,这个方法上的 T 不是泛型类上的 T,将这里的 T 换成其他字母也是一样public static <T> T get(T t) {return t;}
}

但是类型参数可以出现在泛型方法上。

可能会导致泛型类中的方法冲突

    // 类型 T 擦除后会变成 Object,与下一个方法签名相同发生冲突@Overridepublic boolean equals(T t) {return super equals(t);}@Overridepublic boolean equals(Object obj) {return super equals(obj);}

无法创建泛型实例

确切的说是无法通过构造方法创建一个泛型类型的实例,但是通过反射是可以的:

	public static <T> void test(List<T> list, Class<T> cls) {// 通过构造方法创建对象不可以,编译报错T element = new T();// 通过反射调用创建对象是可以的T element1 = cls.newInstance();list.add(element1);}

没有泛型数组

Fruit 是 Apple 的父类,那么 Fruit[] 就是 Apple[] 的父类,这个现象就是数组的协变。假如我们允许泛型数组,Plate<Fruit>[]Plate<Apple>[],类型擦除后没办法满足数组协变的原则,因此 Java 中没有泛型数组。

无法创建,捕获或抛出参数化类型的对象

泛型类不能直接或间接地继承 Throwable 类,例如如下代码会编译报错:

	// Extends Throwable indirectlyclass MathException<T> extends Exception { /* ... */ }    // compile-time error// Extends Throwable directlyclass QueueFullException<T> extends Throwable { /* ... */ // compile-time error

泛型方法不能捕获类型形参的实例:

    public static <T extends Exception, J> void execute(List<J> jobs) {try {for (J job : jobs)// ...} catch (T e) {   // compile-time error// ...}}

但是可以在 throws 子句中使用类型形参:

    class Parser<T extends Exception> {public void parse(File file) throws T {     // OK// ...}}

更多细节可以参考《Java 核心技术卷一》8.6 节“约束与局限性”。

不可具体化类型及其潜在漏洞

具体化类型是其类型信息在运行时完全可用的类型。这包括基本类型,非泛型类型,原始类型和无界通配符的调用。

不可具体化的类型是在编译时通过类型擦除移除信息的类型 - 未定义为无界通配符的泛型类型的调用。不可具体化的类型在运行时没有提供所有信息,该类型的示例是 List<String>List<Number>,JVM 无法在运行时区分这些类型。在某些情况下,不能使用不可具体化类型:例如,在 instanceof 表达式中,或作为数组中的元素。

具有不可具体化的形式参数的 Varargs 方法的潜在漏洞,包含 vararg 输入参数的泛型方法可能会导致堆污染。比如:

public class ArrayBuilder {public static <T> void addToList (List<T> listArg, T... elements) {for (T x : elements) {listArg.add(x);}}public static void faultyMethod(List<String>... l) {Object[] objectArray = l;     // ValidobjectArray[0] = Arrays.asList(42);String s = l[0].get(0);       // ClassCastException thrown here}}

编译时 addToList() 的定义会产生警告:

warning: [varargs] Possible heap pollution from parameterized vararg type T

这是因为,当编译器遇到 varargs 方法时,它会将 varargs 形式参数转换为数组。但是,Java 编程语言不允许创建参数化类型的数组。对于参数 T… elements,会先转换成 T[] elements,经过类型擦除后,变为 Object[] elements,所以才可以直接像 Object[] objectArray = l 这样赋值,但是这样可能会引入堆污染……

5、思考题

考虑如下代码的返回值为何必须强转为 T:

	public <T extends ViewGroup> T get() { //CAP#1 标记匹配,匹配不上就会编译报错return (T)new LinearLayout();}

参考资料

Oracle 官方文档

中文翻译

Java泛型中的桥方法

Java-桥方法

java安全编码指南之:堆污染Heap pollution

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/172590.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微机原理_5

一、单项选择题(本大题共15小题,每小题3分,共45分。在每小题给出的四个备选项中,选出一个正确的答案,请将选定的答案填涂在答题纸的相应位置上。) 8086微处理器CLK引脚输入时钟信号是由(提供。 A. 8284 B. 8288 C.8287 D. 8289 2.下面4个寄存器中,不能作为间接寻址的寄存器是(…

Java + openCV更换证件照背景色

最近在小红书上看到很多更换证件照背景色的需求&#xff0c;联想到以前自己也更换过证件照背景色而且还是付费的&#xff0c;碰巧最近在看一本书《JavaOpenCV高效入门》&#xff0c;于是查找资料&#xff0c;找到了通过技术解决这个需求的办法。 先看效果图&#xff08;图片来自…

62 权限提升-烂土豆dll劫持引号路径服务权限

目录 演示案例:Win2012-烂士豆配合令牌窃取提权-Web权限Win2012-DLL劫持提权应用配合MSF-Web权限Win2012-不安全的服务权限配合MSF-本地权限Win2012-不带引号服务路径配合MSF-Web&#xff0c;本地权限补充说明: dll劫持提权及AlwaysInstallElevated等说明关于Windows相关知识点…

yo!这里是异常相关介绍

目录 前言 异常的概念 异常的抛出与捕获 捕获过程 重新抛出 规范 异常体系 自定义 标准库 异常的优缺点 后记 前言 对于程序运行时发生的错误&#xff0c;比如内存错误、除0错误等类型&#xff0c;你会如何处理&#xff1f;是使用assert终止程序或是使用exit返回错误…

Linux系统---僵尸进程、孤儿进程

顾得泉&#xff1a;个人主页 个人专栏&#xff1a;《Linux操作系统》 《C/C》 键盘敲烂&#xff0c;年薪百万&#xff01; 有了上一篇博客的学习&#xff0c;我们已经简单了解了进程的基础知识&#xff0c;今天我们再来学习两个特殊的进程&#xff0c;僵尸进程和孤儿进程。 …

7000字详解 动态代理(JDK动态代理 CGLIB动态代理)与静态代理

代理模式 1. 代理模式 概念2. 静态代理3. 动态代理3.1.JDK动态代理3.2.CGLIB动态代理3.3. JDK动态代理和CGLIB动态代理区别 4.静态代理和动态代理区别5.篇末 1. 代理模式 概念 代理模式是一种设计模式。 使用代理对象来替代真实对象&#xff0c;用代理对象去访问目标对象。这样…

虚拟化逻辑架构: LBR 网桥基础管理

目录 一、理论 1.Linux Bridge 二、实验 1.LBR 网桥管理 三、问题 1.Linux虚拟交换机如何增删 一、理论 1.Linux Bridge Linux Bridge&#xff08;网桥&#xff09;是用纯软件实现的虚拟交换机&#xff0c;有着和物理交换机相同的功能&#xff0c;例如二层交换&#…

百面深度学习-自然语言处理

自然语言处理 神经机器翻译模型经历了哪些主要的结构变化&#xff1f;分别解决了哪些问题&#xff1f; 神经机器翻译&#xff08;Neural Machine Translation, NMT&#xff09;是一种使用深度学习技术来实现自动翻译的方法。自从提出以来&#xff0c;NMT模型经历了几个重要的…

HTTP协议发展

HTTP 1.0 -> HTTP 1.1 -> HTTP 2.0 -> HTTP 3.0 (QUIC) 每一代HTTP解决了什么问题&#xff1f; 下图说明了主要功能。 HTTP 1.0 于 1996 年最终确定并完整记录。对同一服务器的每个请求都需要单独的 TCP 连接。 HTTP 1.1 于 1997 年发布。TCP 连接可以保持打开状态…

openGauss学习笔记-132 openGauss 数据库运维-查看openGauss状态

文章目录 openGauss学习笔记-132 openGauss 数据库运维-查看openGauss状态132.1 背景信息132.2 前提条件132.3 操作步骤132.4 参数说明132.5 示例 openGauss学习笔记-132 openGauss 数据库运维-查看openGauss状态 132.1 背景信息 openGauss支持查看整个openGauss的状态&#…

如何在Linux系统安装Nginx并启动

Nginx的介绍 Nginx是一款轻量级的Web服务器/反向代理服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器。其特点是占有内存少&#xff0c;并发能力强&#xff0c;事实上nginx的并发能力在同类型的网页服务器中表现较好。官网&#xff1a;nginx newsNginx的下载 前往…

docker基础学习笔记

文章目录 Docker简介Linux下安装DockerDocker常用命令Docker网络Docker存储docker-composedockerfile制作镜像私有仓库镜像导入导出参考 Docker简介 定义&#xff1a;Docker是一个开源的应用容器引擎优势&#xff1a; 一键部署&#xff0c;开箱即用&#xff1a;容器使用基于im…

Qt5.15.2静态编译 VS2017 with static OpenSSL

几年前编译过一次Qt静态库:VS2015编译Qt5.7.0生成支持XP的静态库,再次编译,毫无压力。 一.环境 系统:Windows 10 专业版 64位 编译器:visual studio 2017 第三方工具:perl,ruby和python python用最新的3.x.x版本也是可以的 这三个工具都需要添加到环境变量,安装时勾选…

057-第三代软件开发-文件监视器

第三代软件开发-文件监视器 文章目录 第三代软件开发-文件监视器项目介绍文件监视器实现原理关于 QFileSystemWatcher实现代码 关键字&#xff1a; Qt、 Qml、 关键字3、 关键字4、 关键字5 项目介绍 欢迎来到我们的 QML & C 项目&#xff01;这个项目结合了 QML&…

人工智能时代的内容写作

内容不再只是王道&#xff0c;正如俗话所说&#xff1a;它是一种流动的货币&#xff0c;推动了巨大的在线信息和影响力经济。 每个品牌都是一个故事&#xff0c;通过其服务和商品讲述自己。尽管如此&#xff0c;大多数客户还是会通过您的在线内容最了解您。 但随着我们进入人…

每日一题:LeetCode-LCR 143.子结构判断

每日一题系列&#xff08;day 05&#xff09; 前言&#xff1a; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f50e…

汇编:关于栈的知识

1.入栈和出栈指令 2. SS与SP 3. 入栈与出栈 3.1 执行push ax ↑↑ 3.2 执行pop ax ↓↓ 3.3 栈顶超界的问题 4. 寄存器赋值 基于8086CPU编程时&#xff0c;可以将一段内存当作栈来使用。一个栈段最大可以设为64KB&#xff08;0-FFFFH&#xff09;。 1.入栈和出栈指令…

C语言——函数

导读 &#xff1a; 这篇文章主要讲解一下C语言函数的一些基本知识。 前言&#xff1a;函数的概念 C语言中的函数又常常被称为子程序&#xff0c;是用来完成某项特定的工作的一段代码。就像我们生活中的模块化建造技术&#xff0c;类比模块化建房子的过程&#xff1a;整个程序…

高校大学校园后勤移动报修系统 微信小程序uniapp+vue

本文主要是针对线下校园后勤移动报修传统管理方式中管理不便与效率低的缺点&#xff0c;将电子商务和计算机技术结合起来&#xff0c;开发出管理便捷&#xff0c;效率高的基于app的大学校园后勤移动报修app。该系统、操作简单、界面友好、易于管理和维护&#xff1b;而且对后勤…

Python中类的定义和使用细讲

文章目录 前言一、定义类二、创建类的实例三、创建 _ _ init _ _() 方法四、创建类的成员并访问1. 创建实例方法并访问2. 创建数据成员并访问 五、访问限制 前言 在 Python 中&#xff0c;类表示具有相同属性和方法的对象的集合。在使用类时&#xff0c;需要先定义类&#xff0…