Java基础--泛型篇

版权声明:如有转载-请加微信457556886告知下就可以 知识都是分享的 https://blog.csdn.net/wolf_love666/article/details/90015594

归纳小结:

泛型

  • 本质是类型参数化,解决不确定具体对象类型的问题。
  • 约定规则:
  • E代表元素,T代表对象的类型。K代表某个key,V代表某个value
  • 类型擦除-----CHECKCAST会在运行时候检查对象实例的类型是否匹配,如果不匹配则抛出运行时候的异常ClassCastException。编译期的检查。
  • 使用泛型的好处:
  • 类型安全,不用担心会抛出类型转换异常
  • 没有泛型的代码片段需要强制转换
  • 提升可读性,从编码阶段就知道泛型集合,泛型方法等处理的对象类型是什么
  • 编译时更强的类型检查。
    Java编译器对泛型代码应用强类型检查,如果代码违反了类型安全性,就会发出错误。修复编译时错误比修复运行时错误更容易,而运行时错误可能很难找到
  • 代码重用,泛型合并了同类型的处理代码,代码重用性提高。

小诚信驿站梳理的学习目录:

  • 为什么使用泛型
  • 泛型类型
  • 原生类型
  • 泛型方法
  • 类型参数有界类型
  • 泛型方法和有界类型参数
  • 泛型、继承和子类型
  • 类型推断
  • 通配符
  • 向上有界通配符
  • 无界通配符
  • 向下有界通配符
  • 通配符和子类型
  • 通配符的捕获和帮助方法
  • 通配符的使用指南
  • 类型擦除
  • 泛型类型擦除
  • 泛型方法擦除
  • 类型擦除的影响和桥方法
  • 非具体化类型
  • 限制泛型
  • 小测试

知识点学习

  • 为什么使用泛型
  • 见小结的总结,使用泛型的好处
  • 泛型类型
如果不使用泛型操作一个任意类型的对象可以定义成如下例子:
public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}
这里面隐藏的问题就出来了,由于Object对象所以我们可以set和get相同的任意对象,假设现在set的类型为Integer,然后我get的时候进行了cast类型String这样就会运行异常,编译的时候并不能发现。

现在定义一个不同参数变量的泛型类可以如下格式
class name<T1, T2, ..., Tn> { /* ... */ }
所以上面Box类通过泛型改写下可以改成如下格式
/**
 * Generic version of the Box class.
 * @param <T> the type of the value being boxed
 */
public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}
之前所有的Object对象现在被T代替,而泛型不只可以修饰类还可以修饰接口,数组和其他类 

类型参数命名约定:
为了区分普通类,接口的变量命名,泛型参数命名为单个大写的字母
最常用的参数名是:
E - Element (used extensively by the Java Collections Framework)
K - Key
N - Number
T - Type
V - Value
S,U,V etc. - 2nd, 3rd, 4th types

调用和实例化泛型类型
比如上面的Box的实例化,需要申明泛型可以为Integer或者等等其他类型

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

空的菱形
Java7+以上的版本可以将实例化的泛型类型去除掉,唯一的影响就是编译的时间长短问题,编译器可以通过上下文推断该泛型类型
Box<Integer> integerBox = new Box<>();
多类型参数

申明接口和实现类
public interface Pair<K, V> {
    public K getKey();
    public V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
	this.key = key;
	this.value = value;
    }

    public K getKey()	{ return key; }
    public V getValue() { return value; }
}
实例化方式如下:
Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String>  p2 = new OrderedPair<String, String>("hello", "world");

还可以类型参数化
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
  • 原生类型(泛型类或者接口没有任何参数,称为原生类型)
上面说的实例化一个Box泛型实例
Box<Integer> intBox = new Box<>();
假设现在没有申明泛型类型,则就创建了一个原生类型的Box实例如下
Box rawBox = new Box();

JDK5之前都是原生类型的使用,为了向后高版本兼容,
所以我们可以这样申明泛型以后,再赋值给原生类型
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;               // OK
那么相反如果你把原生类型赋值给类型参数化的,则会提示未检查异常,可能会出现转换异常
Box rawBox = new Box();           // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox;     // warning: unchecked conversion
还有一种情况就是类型参数化的泛型变量赋值给原生类型,原生类型直接调用方法执行,也会出现警告,出现转换异常
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8);  // warning: unchecked invocation to set(T)

所以在使用的过程尽量避免使用原生类型
如果你看到如下提示信息,则是有地方混合使用泛型类型和原生类型

Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

比如如下例子:
public class WarningDemo {
    public static void main(String[] args){
        Box<Integer> bi;
        bi = createBox();
    }

    static Box createBox(){
        return new Box();
    }
}

如果你的项目是好多手历史老旧项目可能会普遍存在这个问题:你可以通过-Xlint:unchecked来获取所有警告:执行完毕出现如下信息:
WarningDemo.java:4: warning: [unchecked] unchecked conversion
found   : Box
required: Box<java.lang.Integer>
        bi = createBox();
                      ^
1 warning
假设要抑制不看这些警告的话可以编译的时候添加给参数: -Xlint:-unchecked ,或者使用上篇注解篇中提到的注解参数@SuppressWarnings("unchecked") 

  • 泛型方法
    与泛型类型用法相似,可以使用的地方静态的、非静态的或者构造器
public class Util {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

public class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}
如何实例化使用可以用如下两种方式都可以
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
或者这种方式:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);
这里省略了泛型类型的申明,其实就是利用了编译器 通过上下文类型推断的特性
  • 类型参数有界
    如果我们现在想要规定传入的泛型只能是某一种规定的类型,则这个时候我们需要进行如下例子操作:
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));
        integerBox.inspect("some text"); // error: this is still String!
    }
}

多边界泛型限制
package com.xiaochengxinyizhan.generics;

/**
 * 多边界泛型
 *
 * @author xiaochengxinyizhan
 * @create 2019-05-10 11:23
 **/
public class MultiBoundsGenerics <T extends A & B & C>{
}
abstract class A{
    abstract void add();
}
interface B{
     void del();
}
interface C{
    void select();
}
class Hello{

}
class World extends A implements B,C{
    @Override
    void add() {
        
    }

    @Override
    void del() {

    }

    @Override
    public void select() {
        
    }
}
class DemoTest{
    MultiBoundsGenerics<Hello> multiBoundsGenerics= new MultiBoundsGenerics<Hello>();
Hello提示编译错误
    MultiBoundsGenerics<World> multiBoundsGeneric2s= new MultiBoundsGenerics<World>();
}
,但是边界顺序必须有序,extends 类然后&接口格式

  • 泛型方法和有界类型参数
    在数组的使用过程中会出现可能申明的泛型为数组类型但是数组传入的比较方式是无法保证传入的是数值类型的:所以这提供一个编译错误的代码:
public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}
那么如何修改呢可以利用CompareTo方法
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}
  • 泛型、继承和子类型
只要类型是兼容的,就可以将一种类型的对象
分配给另一种类型的对象,因为Object是Integer的超类
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger;   // OK
在面向对象的术语中,这称为“is a”关系。
由于整数是一种对象,所以允许赋值。
但是Integer也是一种数字,所以下面的代码也是有效的
public void someMethod(Number n) { /* ... */ }

someMethod(new Integer(10));   // OK
someMethod(new Double(10.1));   // OK

所以在泛型中这样也是可以的
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>或者Box<Double>可以作为参数传进来呢?
答案是否定的:我们可以看下这个关系图
可以看到Box<Integer>和Box<Number>没有任何关系,
但是如果泛型申明的是Object,则Box<Integer>和Box<Number>可以作为参数传入。

在这里插入图片描述

泛型类和子类型
通过扩展或实现泛型类或接口,可以对其进行子类化,
一个类或接口的类型参数与另一个类或接口的类型参数之间的关系由extends和implementation子句确定
比如集合类中的泛型使用

在这里插入图片描述

我们这里可以根据如上规则来进行下定义
interface PayloadList<E,P> extends List<E> {
  void setPayload(int index, P val);
  ...
}
List<String>类型的子类参数化
PayloadList<String,String>
PayloadList<String,Integer>
PayloadList<String,Exception>
关系如下图

在这里插入图片描述

  • 类型推断
    类型推断是Java编译器查看每个方法调用和相应声明的能力,以确定使调用适用的类型参数(或多个参数)。推理算法确定参数的类型,如果可用,还确定分配或返回结果的类型。最后,推理算法尝试找到与所有参数一起工作的最特定类型。
static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());
比如上面的这个例子,是根据第二个参数推断出的T的泛型类型的特定唯一类型是Serializable.

类型推断和泛型方法
泛型方法中介绍了类型推断,可以是你执行一个泛型方法如同执行一个普通方法一样。
比如下面的例子
public class BoxDemo {

  public static <U> void addBox(U u, 
      java.util.List<Box<U>> boxes) {
    Box<U> box = new Box<>();
    box.set(u);
    boxes.add(box);
  }

  public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
    int counter = 0;
    for (Box<U> box: boxes) {
      U boxContents = box.get();
      System.out.println("Box #" + counter + " contains [" +
             boxContents.toString() + "]");
      counter++;
    }
  }

  public static void main(String[] args) {
    java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
      new java.util.ArrayList<>();
    BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
    BoxDemo.outputBoxes(listOfIntegerBoxes);
  }
}
例子的输出结果:
Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]
对于上面的例子由于有了泛型类型推断,那么
原本需要类型修饰的泛型BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);可以直接改为如下方式
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
类型推断和泛型类的实例化
Map<String, List<String>> myMap = new HashMap<String, List<String>>();
Map<String, List<String>> myMap = new HashMap<>();
Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning
这三种都可以但是第三种会提示警告信息是因为由于没有菱形<>来修饰,则编译器不知道是原生类型还是泛型类型。


泛型和非泛型类的类型推断和泛型构造函数
1)注意构造函数在泛型和非泛型类中都可以是泛型的
(换句话说,可以声明它们自己的形式类型参数)。比如如下例子类是X,而构造函数是T
class MyClass<X> {
  <T> MyClass(T t) {
    // ...
  }
}
假设现在实例化
new MyClass<Integer>("")
则X--推断Integer,T推断字符串

目标类型
Java编译器利用目标类型推断泛型方法调用的类型参数。表达式的目标类型是Java编译器期望的数据类型,这取决于表达式出现的位置
假设有这样的一个方法集合返回
static <T> List<T> emptyList();、
编码的时候代码如下
List<String> listOne = Collections.emptyList();
那么java7和java8可以在编译的时候通过类型推断为如下的目标类型
List<String> listOne = Collections.<String>emptyList();
在java7和java8中需要注意的一点:
方法如下:
void processStringList(List<String> stringList) {
    // process stringList
}
编码如下
processStringList(Collections.emptyList());
java7编译的时候提示
List<Object> cannot be converted to List<String>
需要修改为processStringList(Collections.<String>emptyList());
java8编译的时候可以直接通过,原因是在java8中把参数推断,变成了包含参数推断。
  • 通配符
    代码中的?代表了通配符,但是从未应用给泛型
    可以修饰属性、本地变量或者返回值类型。
  • 向上有界通配符
    比如如下例子
public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}
List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));
List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));
在使用过程中,各种属于Number的子类型都可以使用。
  • 无界通配符
无界通配符的两种使用场景
1)使用Object类中提供的功能来实现的方法
2)代码使用中不包括泛型使用
public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

由于不依赖泛型类型,则可以直接用通配符修饰
public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}
代码输出
List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
但是需要注意的是:
 List<Object> and List<?> 是不同的,Object类型可以插入任意类型子类,但是通配符?号只能接受null值插入。
  • 向下有界通配符
与上面相似,使用通配符?号和关键字super来修饰如下例子
public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}
  • 通配符和子类型
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>
如下图关系,虽然Integer是Number的子类型,但List<Integer>不是List<Number>的子类型,事实上,这两种类型并不相关。List<Number>和List<Integer>的共同父节点是List<?>。

在这里插入图片描述
在这里插入图片描述

  • 通配符的捕获和帮助方法
    在这里插入图片描述
    编译器将i输入参数处理为Object类型。当foo方法调用List时。设置(int, E),编译器无法确认插入列表的对象的类型,并产生一个错误。当这种类型的错误发生时,通常意味着编译器认为您为变量分配了错误的类型。由于这个原因,泛型被添加到Java语言中——以在编译时加强类型安全。
    在这里插入图片描述
  • 通配符的使用指南
    “in”变量使用一个上界通配符(使用extends关键字)定义。
    “out”变量使用一个下界通配符(使用super关键字)定义。
    如果可以使用对象类中定义的方法访问“In”变量,则使用无界通配符。
    在代码需要同时以“In”和“out”变量访问变量的情况下,不要使用通配符。
  • 类型擦除
  • 应用场景:
  • 如果类型参数是无界的,则用它们的边界或对象替换泛型类型中的所有类型参数。因此,生成的字节码只包含普通类、接口和方法。
  • 如果需要,插入类型强制转换,以确保类型安全。
  • 生成桥接方法来保存扩展泛型类型中的多态性
  • 类型擦除确保不会为参数化类型创建新类;因此,泛型不会产生运行时开销
  • 泛型类型擦除
  • 在类型擦除过程中,Java编译器擦除所有类型参数,如果类型参数是有界的,则用它的第一个绑定替换每个参数,如果类型参数是无界的,则用对象替换。
    考虑下面的泛型类,它表示单链表中的节点
public class Node<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}
因为类型参数T是无界的,所以Java编译器用Object替换它
public class Node {

    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() { return data; }
    // ...
}
在下面的示例中,泛型节点类使用有界类型参数:
public class Node<T extends Comparable<T>> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}
Java编译器使用第一个绑定类Comparable替换有界类型参数T
public class Node {

    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() { return data; }
    // ...
}
  • 泛型方法擦除
Java编译器还删除泛型方法参数中的类型参数。考虑以下通用方法:
// Counts the number of occurrences of elem in anArray.
//
public static <T> int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}
因为T是无边界的,所以编译器将object替换T
public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}
假设定义了如下类
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
你可以写一个泛型方法达到通用性
public static <T extends Shape> void draw(T shape) { /* ... */ }
而java 编译器会用shape取代T
public static void draw(Shape shape) { /* ... */ }
  • 类型擦除和桥接方法的效果
    有时类型擦除会导致您可能没有预料到的情况。下面的例子说明了这是如何发生的。这个例子(在桥接方法中描述)显示了编译器有时如何创建一个合成方法,称为桥接方法,作为类型擦除过程的一部分。
public class Node<T> {

    public T data;

    public Node(T data) { this.data = data; }

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}
考虑下如下代码
MyNode mn = new MyNode(5);
Node n = mn;            // A raw type - compiler throws an unchecked warning
n.setData("Hello");     
Integer x = mn.data;    // Causes a ClassCastException to be thrown.
在类型擦除后,代码变为
MyNode mn = new MyNode(5);
Node n = (MyNode)mn;         // A raw type - compiler throws an unchecked warning
n.setData("Hello");
Integer x = (String)mn.data; // Causes a ClassCastException to be thrown.
以下是代码执行时的情况
n.setData (“Hello”);使方法setData(对象)在类MyNode的对象上执行。(MyNode类从Node继承了setData(Object)。)
在setData(Object)的主体中,n引用的对象的数据字段被分配给一个字符串。
通过mn引用的相同对象的数据字段可以被访问,并且被期望为整数(因为mn是一个MyNode,它是一个< integer >节点)。
试图将字符串赋值给整数会导致ClassCastException异常,该异常来自Java编译器在赋值时插入的强制转换。

桥方法:
当编译扩展参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法,称为桥接方法,作为类型擦除过程的一部分。通常不需要担心桥接方法,但是如果堆栈跟踪中出现了桥接方法,可能会很难理解。
在类型擦除以后,这个Node和MyNode类如下:
public class Node {

    public Object data;

    public Node(Object data) { this.data = data; }

    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node {

    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}
类型擦除后,方法签名不匹配。节点方法变成setData(Object), MyNode方法变成setData(Integer)。因此,MyNode setData方法不会覆盖Node setData方法。
为了解决这个问题,并在类型擦除之后保留泛型类型的多态性,Java编译器生成一个桥接方法,以确保子类型按预期工作。对于MyNode类,编译器为setData生成以下桥接方法:
class MyNode extends Node {

    // Bridge method generated by the compiler
    //
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    // ...
}
可以看到,bridge方法在类型擦除之后具有与节点类的setData方法相同的方法签名,它委托给原始的setData方法
  • 非具体化类型
    类型擦除的结果与变量参数(也称为varargs)方法有关,这些方法的varargs形式参数具有不可具体化的类型
  • 非具体化类型
  • 可具体化类型是在运行时类型信息完全可用的类型。这包括基本类型、非泛型类型、原始类型和未绑定通配符的调用。不可具体化类型是在编译时通过对未定义为无界通配符的泛型类型的类型擦除调用来删除信息的类型。非具体化类型在运行时不具有其所有信息。不可具体化类型的例子有List和List;JVM在运行时无法区分这些类型。如对泛型的限制所示,在某些情况下不能使用不可具体化的类型:例如,在instanceof表达式中,或者作为数组中的元素。
  • 堆污染
  • 当参数化类型的变量引用非参数化类型的对象时,会发生堆污染。如果程序执行的某些操作在编译时引发未检查的警告,则会发生这种情况。如果在编译时(在编译时类型检查规则的限制范围内)或运行时无法验证涉及参数化类型的操作(例如,转换或方法调用)的正确性,则会生成未检查警告。例如,当混合原始类型和参数化类型时,或者执行未检查的强制转换时,会发生堆污染。在正常情况下,当所有代码同时编译时,编译器会发出未选中警告,以提醒您注意潜在的堆污染。如果单独编译代码的各个部分,就很难检测堆污染的潜在风险。如果您确保您的代码在编译时没有警告,那么就不会发生堆污染。
  • 具有不可具体化形式参数的变参数方法的潜在漏洞
  • 包含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;     // Valid
    objectArray[0] = Arrays.asList(42);
    String s = l[0].get(0);       // ClassCastException thrown here
  }

}
如下使用上面的例子造成的堆污染
public class HeapPollutionExample {

  public static void main(String[] args) {

    List<String> stringListA = new ArrayList<String>();
    List<String> stringListB = new ArrayList<String>();

    ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
    ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
    List<List<String>> listOfStringLists =
      new ArrayList<List<String>>();
    ArrayBuilder.addToList(listOfStringLists,
      stringListA, stringListB);

    ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
  }
}
程序编译的时候会产生如下警告信息
warning: [varargs] Possible heap pollution from parameterized vararg type T
当编译器遇到varargs方法时,它将varargs形式参数转换为数组。
但是,Java编程语言不允许创建参数化类型数组。在方法ArrayBuilder中。
编译器将varargs的形式参数T…元素的形式参数T[]元素,数组。
但是,由于类型擦除,编译器将varargs形式参数转换为Object[]元素。
因此,存在堆污染的可能性。
下面的语句将varargs形式参数l分配给对象数组
Object[] objectArray = l;
这个语句可能会引入堆污染。与varargs形式参数l的参数化类型匹配的值可以分配给变量objectArray,
因此可以分配给l。但是,编译器不会在此语句中生成未选中的警告。
编译器在翻译varargs形式参数列表<String>…l.该语句是有效的;变量l具有类型列表[],
它是Object[]的子类型。
因此,如果您将任何类型的列表对象分配给objectArray数组的任何数组组件,
编译器不会发出警告或错误,如下面的语句所示
objectArray[0] = Arrays.asList(42);
此语句将一个列表对象分配给objectArray数组的第一个数组组件,该列表对象包含一个Integer类型的对象。
假设你现在执行这个语句
ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
在运行的时候,JVm会抛出类转换异常
// ClassCastException thrown here
String s = l[0].get(0);
变量l的第一个数组组件中存储的对象具有类型列表<Integer>,但是该语句期望类型列表<String>的对象


防止来自具有不可具体化形式参数的Varargs方法的警告
如果你声明一个varargs方法参数的一个参数化的类型,你确保方法的身体不会抛出一个ClassCastException异常或其他类似的异常,由于处理不当的可变参数形式参数,可以避免编译器生成的警告,这些可变参数方法通过添加以下注释静态和non-constructor方法声明
@SafeVarargs
@SafeVarargs注释是方法契约的文档部分;该注释断言方法的实现不会不正确地处理varargs形式参数。
也可以通过在方法声明中添加以下内容来抑制这种警告,虽然不推荐
@SuppressWarnings({"unchecked", "varargs"})
但是,这种方法不会禁止从方法的调用站点生成警告。
  • 泛型的限制规范
  • 无法用基本类型实例化泛型类型
  • 无法创建类型参数的实例
  • 不能声明类型为类型参数的静态字段
  • 不能对参数化类型使用强制类型转换或instanceof
  • 无法创建参数化类型数组
  • 无法创建、捕获或抛出参数化类型的对象
  • 无法重载将每个重载的形式参数类型擦除为相同原始类型的方法
无法用基本类型实例化泛型类型
class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    // ...
}
假设现在实例化基本变量
Pair<int, char> p = new Pair<>(8, 'a');  // compile-time error
您只能用非基本类型替换类型参数K和V
Pair<Integer, Character> p = new Pair<>(8, 'a');
注意,Java编译器自动从8到整数. valueof(8)和从a到字符(a):
Pair<Integer, Character> p = new Pair<>(Integer.valueOf(8), new Character('a'));
无法创建类型参数的实例
public static <E> void append(List<E> list) {
    E elem = new E();  // compile-time error
    list.add(elem);
}
可以通过反射获取
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.newInstance();   // OK
    list.add(elem);
}
然后执行该方法
List<String> ls = new ArrayList<>();
append(ls, String.class);
不能声明类型为类型参数的静态字段
类的静态字段是类的所有非静态对象共享的类级变量。因此,类型参数的静态字段是不允许的。
public class MobileDevice<T> {
    private static T os;

    // ...
}

MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();
因为静态字段os由电话、寻呼机和pc共享,所以操作系统的实际类型是什么?它不能同时是智能手机、寻呼机和平板电脑。因此,您不能创建类型参数的静态字段。
不能对参数化类型使用强制类型转换或instanceof
因为Java编译器会擦除泛型代码中的所有类型参数,所以您无法验证在运行时使用泛型类型的哪个参数化类型
public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // compile-time error
        // ...
    }
}
传递给rtti方法的参数化类型集合为
S = { ArrayList<Integer>, ArrayList<String> LinkedList<Character>, ... }
运行时不跟踪类型参数,因此它无法区分ArrayList<Integer>和ArrayList<String>之间的区别。您最多可以使用无界通配符来验证列表是否是ArrayList
public static void rtti(List<?> list) {
    if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
        // ...
    }
}
通常,您不能转换为参数化类型,除非它是由无界通配符参数化的。例如
List<Integer> li = new ArrayList<>();
List<Number>  ln = (List<Number>) li;  // compile-time error
然而,在某些情况下,编译器知道类型参数总是有效的,并且允许强制转换。例如:
List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>)l1;  // OK
 
无法创建参数化类型数组
List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error
下面的代码演示了在数组中插入不同类型时会发生什么
Object[] strings = new String[2];
strings[0] = "hi";   // OK
strings[1] = 100;    // An ArrayStoreException is thrown.
如果你试着用一个通用列表做同样的事情,会有一个问题
Object[] stringLists = new List<String>[];  // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>();   // OK
stringLists[1] = new ArrayList<Integer>();  // An ArrayStoreException should be thrown,
                                            // but the runtime can't detect it.
 如果允许参数化列表数组,前面的代码将无法抛出所需的ArrayStoreException。
无法创建、捕获或抛出参数化类型的对象
泛型类不能直接或间接扩展可抛出类。例如,下面的类将不会编译
// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ }    // compile-time error

// Extends Throwable directly
class 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
        // ...
    }
}
但是,您可以在抛出子句中使用类型参数:
class Parser<T extends Exception> {
    public void parse(File file) throws T {     // OK
        // ...
    }
}
无法重载将每个重载的形式参数类型擦除为相同原始类型的方法
类不能有两个重载方法,它们在类型擦除之后具有相同的签名
public class Example {
    public void print(Set<String> strSet) { }
    public void print(Set<Integer> intSet) { }
}
重载将共享相同的类文件表示,并将生成编译时错误
  • 小测试
    1)编写一个泛型方法来计算集合中具有特定属性(例如,奇数、素数、回文)的元素数量。
    A:答案
Answer:
public final class Algorithm {
    public static <T> int countIf(Collection<T> c, UnaryPredicate<T> p) {

        int count = 0;
        for (T elem : c)
            if (p.test(elem))
                ++count;
        return count;
    }
}
where the generic UnaryPredicate interface is defined as follows:
public interface UnaryPredicate<T> {
    public boolean test(T obj);
}
For example, the following program counts the number of odd integers in an integer list:
import java.util.*;

class OddPredicate implements UnaryPredicate<Integer> {
    public boolean test(Integer i) { return i % 2 != 0; }
}

public class Test {
    public static void main(String[] args) {
        Collection<Integer> ci = Arrays.asList(1, 2, 3, 4);
        int count = Algorithm.countIf(ci, new OddPredicate());
        System.out.println("Number of odd integers = " + count);
    }
}
The program prints:
Number of odd integers = 2

2)下述代码是否可以正常编译,如果不可以为什么?

public final class Algorithm {
    public static <T> T max(T x, T y) {
        return x > y ? x : y;
    }
}

不可以:大于(>)运算符只适用于基本数值类型

3)写一个泛型方法交换一个数组中的2个不同的元素位置

public final class Algorithm {
    public static <T> void swap(T[] a, int i, int j) {
        T temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

4)如果编译器在编译时擦除所有类型参数,为什么要使用泛型

您应该使用泛型,因为:
Java编译器在编译时强制对泛型代码进行更严格的类型检查。
泛型支持将编程类型作为参数。
泛型使您能够实现泛型算法

5)以下类在类型擦除之后转换为什么?

public class Pair<K, V> {

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey(); { return key; }
    public V getValue(); { return value; }

    public void setKey(K key)     { this.key = key; }
    public void setValue(V value) { this.value = value; }

    private K key;
    private V value;
}

答案为:

public class Pair {

    public Pair(Object key, Object value) {
        this.key = key;
        this.value = value;
    }

    public Object getKey()   { return key; }
    public Object getValue() { return value; }

    public void setKey(Object key)     { this.key = key; }
    public void setValue(Object value) { this.value = value; }

    private Object key;
    private Object value;
}

6)在下列类型擦除后下列方法转换为什么?

public static <T extends Comparable<T>>
    int findFirstGreaterThan(T[] at, T elem) {
    // ...
}

答案为:

public static int findFirstGreaterThan(Comparable[] at, Comparable elem) {
    // ...
    }

7)下面的方法是否可正常编译,如果不可以为什么?

public static void print(List<? extends Number> list) {
    for (Number n : list)
        System.out.print(n + " ");
    System.out.println();
}

答案为:yes

8)写一个泛型方法查找list的[rang,end)中的最大值
答案为:

import java.util.*;

public final class Algorithm {
    public static <T extends Object & Comparable<? super T>>
        T max(List<? extends T> list, int begin, int end) {

        T maxElem = list.get(begin);

        for (++begin; begin < end; ++begin)
            if (maxElem.compareTo(list.get(begin)) < 0)
                maxElem = list.get(begin);
        return maxElem;
    }
}

9)下面代码是否可以正常编译

public class Singleton<T> {

    public static T getInstance() {
        if (instance == null)
            instance = new Singleton<T>();

        return instance;
    }

    private static T instance = null;
}

答案为:不可以,不能创建类型参数T的静态字段

10)如下代码,哪些能编译,为什么?

类如下:
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

class Node<T> { /* ... */ }

是否可以编译下面的两行代码
Node<Circle> nc = new Node<>();
Node<Shape>  ns = nc;

答案为;不。因为节点 不是节点的子类型

11)考虑是否正常编译

类如下
class Node<T> implements Comparable<T> {
    public int compareTo(T obj) { /* ... */ }
    // ...
}
是否正常编译
Node<String> node = new Node<>();
Comparable<String> comp = node;

答案为:yes

12)如何调用下面的方法来查找列表中相对于指定整数列表来说是素数的第一个整数?

public static <T>
    int findFirst(List<T> list, int begin, int end, UnaryPredicate<T> p)
    注意,如果gcd(a, b) = 1,则两个整数a和b是相对质数,其中gcd是最大公约数的缩写。

答案为:

import java.util.*;

public final class Algorithm {

    public static <T>
        int findFirst(List<T> list, int begin, int end, UnaryPredicate<T> p) {

        for (; begin < end; ++begin)
            if (p.test(list.get(begin)))
                return begin;
        return -1;
    }

    // x > 0 and y > 0
    public static int gcd(int x, int y) {
        for (int r; (r = x % y) != 0; x = y, y = r) { }
            return y;
    }
}
The generic UnaryPredicate interface is defined as follows:
public interface UnaryPredicate<T> {
    public boolean test(T obj);
}
The following program tests the findFirst method:
import java.util.*;

class RelativelyPrimePredicate implements UnaryPredicate<Integer> {
    public RelativelyPrimePredicate(Collection<Integer> c) {
        this.c = c;
    }

    public boolean test(Integer x) {
        for (Integer i : c)
            if (Algorithm.gcd(x, i) != 1)
                return false;

        return c.size() > 0;
    }

    private Collection<Integer> c;
}

public class Test {
    public static void main(String[] args) throws Exception {

        List<Integer> li = Arrays.asList(3, 4, 6, 8, 11, 15, 28, 32);
        Collection<Integer> c = Arrays.asList(7, 18, 19, 25);
        UnaryPredicate<Integer> p = new RelativelyPrimePredicate(c);

        int i = ALgorithm.findFirst(li, 0, li.size(), p);

        if (i != -1) {
            System.out.print(li.get(i) + " is relatively prime to ");
            for (Integer k : c)
                System.out.print(k + " ");
            System.out.println();
        }
    }
}
The program prints:
11 is relatively prime to 7 18 19 25

猜你喜欢

转载自blog.csdn.net/wolf_love666/article/details/90015594