【廖雪峰官方网站/Java教程】泛型

news/2024/5/20 4:45:32/文章来源:https://blog.csdn.net/Allenlzcoder/article/details/105205929

泛型是一种“代码模板”,可以用一套代码套用各种类型。
在这里插入图片描述

1.什么是泛型

1.1.泛型入门概念介绍

为了在ArrayList中兼容不同类型等元素,我们必须把ArrayList变成一种模板:ArrayList<T>,代码如下:

public class ArrayList<T> {private T[] array;private int size;public void add(T e) {...}public void remove(int index) {...}public T get(int index) {...}
}

T可以是任何class。这样一来,我们就实现了:编写一次模版,可以创建任意类型的ArrayList:

// 创建可以存储String的ArrayList:
ArrayList<String> strList = new ArrayList<String>();
// 创建可以存储Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
// 创建可以存储Person的ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();

因此,泛型就是定义一种模板,例如ArrayList<T>,然后在代码中为用到的类创建对应的ArrayList<类型>:

ArrayList<String> strList = new ArrayList<String>();

由编译器针对类型做检查:

strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!

这样一来,既实现了编写一次,万能匹配,又通过编译器保证了类型安全:这就是泛型。

1.2.向上转型

在Java标准库中的ArrayList<T>实现了List<T>接口,它可以向上转型为List<T>:

public class ArrayList<T> implements List<T> {...
}
List<String> list = new ArrayList<String>();

即类型ArrayList<T>可以向上转型为List<T>(T不能变!)。

2.使用泛型

2.1.用法示例

使用ArrayList时,如果不定义泛型类型时,泛型类型实际上就是Object:

// 编译器警告:
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);

此时,只能把<T>当作Object使用,没有发挥泛型的优势。
当我们定义泛型类型<String>后,List<T>的泛型接口变为强类型List<String>:

// 无编译器警告:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
// 无强制转型:
String first = list.get(0);
String second = list.get(1);

2.2.泛型接口

除了ArrayList<T>使用了泛型,还可以在接口中使用泛型。例如,Arrays.sort(Object[])可以对任意数组进行排序,但待排序的元素必须实现Comparable<T>这个泛型接口:

public interface Comparable<T> {/*** 返回-1: 当前实例比参数o小* 返回0: 当前实例与参数o相等* 返回1: 当前实例比参数o大*/int compareTo(T o);
}

可以直接对String数组进行排序:

// sort
import java.util.Arrays;public class Main {public static void main(String[] args) {String[] ss = new String[] { "Orange", "Apple", "Pear" };Arrays.sort(ss);System.out.println(Arrays.toString(ss));}
}

这是因为String本身已经实现了Comparable<String>接口。如果换成我们自定义的Person类型试试:

// sort
import java.util.Arrays;public class Main {public static void main(String[] args) {Person[] ps = new Person[] {new Person("Bob", 61),new Person("Alice", 88),new Person("Lily", 75),};Arrays.sort(ps);System.out.println(Arrays.toString(ps));}
}class Person implements Comparable<Person> {String name;int score;Person(String name, int score) {this.name = name;this.score = score;}public int compareTo(Person other) {return this.name.compareTo(other.name);}public String toString() {return this.name + "," + this.score;}
}

运行上述代码,可以正确实现按name进行排序。

3.编写泛型

3.1.三步编写泛型

如果我们确实需要编写一个泛型类,那么,应该如何编写它?
可以按照以下步骤来编写一个泛型类。首先,按照某种类型,例如:String,来编写类:

public class Pair {private String first;private String last;public Pair(String first, String last) {this.first = first;this.last = last;}public String getFirst() {return first;}public String getLast() {return last;}
}

然后,标记所有的特定类型,这里是String,最后,把特定类型String替换为T,并申明<T>

public class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() {return first;}public T getLast() {return last;}
}

熟练后即可直接从T开始编写。

3.2.静态方法

编写泛型类时,要特别注意,泛型类型<T>不能用于静态方法。例如:

public class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() { ... }public T getLast() { ... }// 对静态方法使用<T>:public static Pair<T> create(T first, T last) {return new Pair<T>(first, last);}
}

上述代码会导致编译错误,我们无法在静态方法create()的方法参数和返回类型上使用泛型类型T。
有些同学在网上搜索发现,可以在static修饰符后面加一个<T>,编译就能通过:

public class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() { ... }public T getLast() { ... }// 可以编译通过:public static <T> Pair<T> create(T first, T last) {return new Pair<T>(first, last);}
}

但实际上,这个<T>和Pair<T>类型的<T>已经没有任何关系了。
对于静态方法,我们可以单独改写为“泛型”方法,只需要使用另一个类型即可。对于上面的create()静态方法,我们应该把它改为另一种泛型类型,例如,<K>:

public class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() { ... }public T getLast() { ... }// 静态泛型方法应该使用其他类型区分:public static <K> Pair<K> create(K first, K last) {return new Pair<K>(first, last);}
}

这样才能清楚地将静态方法的泛型类型和实例类型的泛型类型区分开。

3.3.多个泛型类型

泛型还可以定义多种类型。例如,我们希望Pair不总是存储两个类型一样的对象,就可以使用类型<T, K>:

public class Pair<T, K> {private T first;private K last;public Pair(T first, K last) {this.first = first;this.last = last;}public T getFirst() { ... }public K getLast() { ... }
}

使用的时候,需要指出两种类型:

Pair<String, Integer> p = new Pair<>("test", 123);

Java标准库的Map<K, V>就是使用两种泛型类型的例子。它对Key使用一种类型,对Value使用另一种类型。

4.擦拭法

4.1.擦拭法基本概念

泛型是一种类似"模板代码"的技术,不同语言的泛型实现方式不一定相同。Java语言的泛型实现方式是擦拭法(Type Erasure)。所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的
例如,我们编写了一个泛型类Pair,这是编译器看到的代码:

public class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() {return first;}public T getLast() {return last;}
}

而虚拟机根本不知道泛型。这是虚拟机执行的代码:

public class Pair {private Object first;private Object last;public Pair(Object first, Object last) {this.first = first;this.last = last;}public Object getFirst() {return first;}public Object getLast() {return last;}
}

因此,Java使用擦拭法实现泛型,导致了:

  • 编译器把类型视为Object;
  • 编译器根据实现安全的强制转型。

使用泛型的时候,我们编写的代码也是编译器看到的代码:

Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();

而虚拟机执行的代码并没有泛型:

Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();

所以,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。

4.2.擦拭法局限

4.2.1.局限1:<T>不能是基本类型

例如int,因为实际类型是Object,Object类型无法持有基本类型:

Pair<int> p = new Pair<>(1, 2); // compile error!

4.2.2.局限2:无法取得带泛型的Class

观察以下代码:

public class Main {public static void main(String[] args) {Pair<String> p1 = new Pair<>("Hello", "world");Pair<Integer> p2 = new Pair<>(123, 456);Class c1 = p1.getClass();Class c2 = p2.getClass();System.out.println(c1 == c2); // trueSystem.out.println(c1 == Pair.class); // true}
}class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() {return first;}public T getLast() {return last;}
}

因为T是Object,我们对Pair<String>和Pair<Integer>类型获取Class时,获取到的是同一个Class,也就是Pair类的Class。
换句话说,所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是Pair<Object>。

4.2.3.局限3:无法判断带泛型的Class

Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>.class) {
}

原因和前面一样,并不存在Pair<String>.class,而是只有唯一的Pair.class。

4.2.4.局限4:不能实例化T类型

public class Pair<T> {private T first;private T last;public Pair() {// Compile error:first = new T();last = new T();}
}

上述代码无法通过编译,因为构造方法的两行语句:

first = new T();
last = new T();

擦拭后实际上变成了:

first = new Object();
last = new Object();

这样一来,创建new Pair()和创建new Pair()就全部成了Object,显然编译器要阻止这种类型不对的代码。
要实例化T类型,我们必须借助额外的Class参数

public class Pair<T> {private T first;private T last;public Pair(Class<T> clazz) {first = clazz.newInstance();last = clazz.newInstance();}
}

上述代码借助Class<T>参数并通过反射来实例化T类型,使用的时候,也必须传入Class<T>。例如:

Pair<String> pair = new Pair<>(String.class);

因为传入了Class的实例,所以我们借助String.class就可以实例化String类型。

4.3.不恰当的覆写方法

有些时候,一个看似正确定义的方法会无法通过编译。例如:

public class Pair<T> {public boolean equals(T t) {return this == t;}
}

这是因为,定义的equals(T t)方法实际上会被擦拭成equals(Object t),而这个方法是继承自Object的,编译器会阻止一个实际上会变成覆写的泛型方法定义。
换个方法名,避开与Object.equals(Object)的冲突就可以成功编译:

public class Pair<T> {public boolean same(T t) {return this == t;}
}

4.4.泛型继承

一个类可以继承自一个泛型类。例如:父类的类型是Pair<Integer>,子类的类型是IntPair,可以这么继承:

public class IntPair extends Pair<Integer> {
}

使用的时候,因为子类IntPair并没有泛型类型,所以,正常使用即可:

IntPair ip = new IntPair(1, 2);

前面讲了,我们无法获取Pair<T>的T类型,即给定一个变量Pair<Integer> p,无法从p中获取到Integer类型。
但是,在父类是泛型类型的情况下,编译器就必须把类型T(对IntPair来说,也就是Integer类型)保存到子类的class文件中,不然编译器就不知道IntPair只能存取Integer这种类型。
在继承了泛型类型的情况下,子类可以获取父类的泛型类型。例如:IntPair可以获取到父类的泛型类型Integer。获取父类的泛型类型代码比较复杂:

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;public class Main {public static void main(String[] args) {Class<IntPair> clazz = IntPair.class;Type t = clazz.getGenericSuperclass();if (t instanceof ParameterizedType) {ParameterizedType pt = (ParameterizedType) t;Type[] types = pt.getActualTypeArguments(); // 可能有多个泛型类型Type firstType = types[0]; // 取第一个泛型类型Class<?> typeClass = (Class<?>) firstType;System.out.println(typeClass); // Integer}}
}class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() {return first;}public T getLast() {return last;}
}class IntPair extends Pair<Integer> {public IntPair(Integer first, Integer last) {super(first, last);}
}

因为Java引入了泛型,所以,只用Class来标识类型已经不够了。实际上,Java的类型系统结构如下:
在这里插入图片描述

4.5.小结

(1)Java的泛型是采用擦拭法实现的;
(2)擦拭法决定了泛型<T>:

  • 不能是基本类型,例如:int;
  • 不能获取带泛型类型的Class,例如:Pair.class;
  • 不能判断带泛型类型的类型,例如:x instanceof Pair;
  • 不能实例化T类型,例如:new T()。

(3)泛型方法要防止重复定义方法,例如:public boolean equals(T obj);
(4)子类可以获取父类的泛型类型。

5.extends通配符

5.1.引入extends通配符

因为Pair<Integer>不是Pair<Number>的子类,有没有办法使得方法参数接受Pair<Integer>?办法是有的,这就是使用Pair<? extends Number>使得方法接收所有泛型类型为Number或Number子类的Pair类型。我们把代码改写如下:

public class Main {public static void main(String[] args) {Pair<Integer> p = new Pair<>(123, 456);int n = add(p);System.out.println(n);}static int add(Pair<? extends Number> p) {Number first = p.getFirst();Number last = p.getLast();return first.intValue() + last.intValue();}}class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() {return first;}public T getLast() {return last;}
}

这样一来,给方法传入Pair<Integer>类型时,它符合参数Pair<? extends Number>类型。这种使用<? extends Number>的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。
除了可以传入Pair<Integer>类型,我们还可以传入Pair<Double>类型,Pair<BigDecimal>类型等等,因为Double和BigDecimal都是Number的子类。
如果我们考察对Pair<? extends Number>类型调用getFirst()方法,实际的方法签名变成了:

<? extends Number> getFirst();

即返回值是Number或Number的子类,因此,可以安全赋值给Number类型的变量:

Number x = p.getFirst();

然后,我们不可预测实际类型就是Integer,例如,下面的代码是无法通过编译的:

Integer x = p.getFirst();

这是因为实际的返回类型可能是Integer,也可能是Double或者其他类型,编译器只能确定类型一定是Number的子类(包括Number类型本身),但具体类型无法确定。
我们再来考察一下Pair<T>的set方法:

public class Main {public static void main(String[] args) {Pair<Integer> p = new Pair<>(123, 456);int n = add(p);System.out.println(n);}static int add(Pair<? extends Number> p) {Number first = p.getFirst();Number last = p.getLast();p.setFirst(new Integer(first.intValue() + 100));p.setLast(new Integer(last.intValue() + 100));return p.getFirst().intValue() + p.getFirst().intValue();}}class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() {return first;}public T getLast() {return last;}public void setFirst(T first) {this.first = first;}public void setLast(T last) {this.last = last;}
}

不出意外,我们会得到一个编译错误:

incompatible types: Integer cannot be converted to CAP#1
where CAP#1 is a fresh type-variable:CAP#1 extends Number from capture of ? extends Number

编译错误发生在p.setFirst()传入的参数是Integer类型。有些童鞋会问了,既然p的定义是Pair<? extends Number>,那么setFirst(? extends Number)为什么不能传入Integer?
原因还在于擦拭法。如果我们传入的p是Pair<Double>,显然它满足参数定义Pair<? extends Number>,然而,Pair<Double>的setFirst()显然无法接受Integer类型。
这就是<? extends Number>通配符的一个重要限制:方法参数签名setFirst(? extends Number)无法传递任何Number类型给setFirst(? extends Number)
这里唯一的例外是可以给方法参数传入null:

p.setFirst(null); // ok, 但是后面会抛出NullPointerException
p.getFirst().intValue(); // NullPointerException

5.2.extends通配符的作用

如果我们考察Java标准库的java.util.List<T>接口,它实现的是一个类似“可变数组”的列表,主要功能包括:

public interface List<T> {int size(); // 获取个数T get(int index); // 根据索引获取指定元素void add(T t); // 添加一个新元素void remove(T t); // 删除一个已有元素
}

现在,让我们定义一个方法来处理列表的每个元素:

int sumOfList(List<? extends Integer> list) {int sum = 0;for (int i=0; i<list.size(); i++) {Integer n = list.get(i);sum = sum + n;}return sum;
}

为什么我们定义的方法参数类型是List<? extends Integer>而不是List<Integer>?从方法内部代码看,传入List<? extends Integer>或者List<Integer>是完全一样的,但是,注意到List<? extends Integer>的限制:

  • 允许调用get()方法获取Integer的引用;
  • 不允许调用set(? extends Integer)方法并传入任何Integer的引用(null除外)。

因此,方法参数类型List<? extends Integer>表明了该方法内部只会读取List的元素,不会修改List的元素(因为无法调用add(? extends Integer)、remove(? extends Integer)这些方法。换句话说,这是一个对参数List<? extends Integer>进行只读的方法(恶意调用set(null)除外)。

5.3.使用extends限定T类型

在定义泛型类型Pair<T>的时候,也可以使用extends通配符来限定T的类型:

public class Pair<T extends Number> { ... }

现在,我们只能定义:

Pair<Number> p1 = null;
Pair<Integer> p2 = new Pair<>(1, 2);
Pair<Double> p3 = null;

因为Number、Integer和Double都符合<T extends Number>。
非Number类型将无法通过编译:

Pair<String> p1 = null; // compile error!
Pair<Object> p2 = null; // compile error!

因为String、Object都不符合<T extends Number>,因为它们不是Number类型或Number的子类。

5.4.小结

使用类似<? extends Number>通配符作为方法参数时表示:

  • 方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();;
  • 方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);。

即一句话总结:使用extends通配符表示可以读,不能写
使用类似<T extends Number>定义泛型类时表示:泛型类型限定为Number以及Number的子类。

6.super通配符

6.1.super通配符使用举例

我们使用super通配符来下面的set(*)这个方法:

void set(Pair<? super Integer> p, Integer first, Integer last) {p.setFirst(first);p.setLast(last);
}

注意到Pair<? super Integer>表示,方法参数接受所有泛型类型为Integer或Integer父类的Pair类型。
下面的代码可以被正常编译:

public class Main {public static void main(String[] args) {Pair<Number> p1 = new Pair<>(12.3, 4.56);Pair<Integer> p2 = new Pair<>(123, 456);setSame(p1, 100);setSame(p2, 200);System.out.println(p1.getFirst() + ", " + p1.getLast());System.out.println(p2.getFirst() + ", " + p2.getLast());}static void setSame(Pair<? super Integer> p, Integer n) {p.setFirst(n);p.setLast(n);}
}class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() {return first;}public T getLast() {return last;}public void setFirst(T first) {this.first = first;}public void setLast(T last) {this.last = last;}
}

考察Pair<? super Integer>的setFirst()方法,它的方法签名实际上是:

void setFirst(? super Integer);

因此,可以安全地传入Integer类型。
再考察Pair<? super Integer>的getFirst()方法,它的方法签名实际上是:

? super Integer getFirst();

这里注意到我们无法使用Integer类型来接收getFirst()的返回值,即下面的语句将无法通过编译:

Integer x = p.getFirst();

因为如果传入的实际类型是Pair<Number>,编译器无法将Number类型转型为Integer。
注意:虽然Number是一个抽象类,我们无法直接实例化它。但是,即便Number不是抽象类,这里仍然无法通过编译。此外,传入Pair<Object>类型时,编译器也无法将Object类型转型为Integer。
唯一可以接收getFirst()方法返回值的是Object类型:

Object obj = p.getFirst();

因此,使用<? super Integer>通配符表示:

  • 允许调用set(? super Integer)方法传入Integer的引用;
  • 不允许调用get()方法获得Integer的引用。

唯一例外是可以获取Object的引用:Object o = p.getFirst()。
换句话说,使用<? super Integer>通配符作为方法参数,表示方法内部代码对于参数只能写,不能读

6.2.对比extends和super通配符

我们再回顾一下extends通配符。作为方法参数,<? extends T>类型和<? super T>类型的区别在于:

  • <? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
  • <? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。

一个(extends)是允许读不允许写,另一个(super)是允许写不允许读
我们来看Java标准库的Collections类定义的copy()方法:

public class Collections {// 把src的每个元素复制到dest中:public static <T> void copy(List<? super T> dest, List<? extends T> src) {for (int i=0; i<src.size(); i++) {T t = src.get(i);dest.add(t);}}
}

它的作用是把一个List的每个元素依次添加到另一个List中。它的第一个参数是List<? super T>,表示目标List,第二个参数List<? extends T>,表示要复制的List。我们可以简单地用for循环实现复制。在for循环中,我们可以看到,对于类型<? extends T>的变量src,我们可以安全地获取类型T的引用,而对于类型<? super T>的变量dest,我们可以安全地传入T的引用。
这个copy()方法的定义就完美地展示了extends和super的意图:

  • copy()方法内部不会读取dest,因为不能调用dest.get()来获取T的引用;

  • copy()方法内部也不会修改src,因为不能调用src.add(T)。

这是由编译器检查来实现的。如果在方法代码中意外修改了src,或者意外读取了dest,就会导致一个编译错误:

public class Collections {// 把src的每个元素复制到dest中:public static <T> void copy(List<? super T> dest, List<? extends T> src) {...T t = dest.get(0); // compile error!src.add(t); // compile error!}
}

这个copy()方法的另一个好处是可以安全地把一个List<Integer>添加到List<Number>,但是无法反过来添加:

// copy List<Integer> to List<Number> ok:
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);// ERROR: cannot copy List<Number> to List<Integer>:
Collections.copy(intList, numList);

而这些都是通过super和extends通配符,并由编译器强制检查来实现的。

6.3.PECS原则

何时使用extends,何时使用super?为了便于记忆,我们可以用PECS原则:Producer Extends Consumer Super。
即:如果需要返回T,它是生产者(Producer),要使用extends通配符;如果需要写入T,它是消费者(Consumer),要使用super通配符

  • 返回T => 生产者(Producer)=> extends
  • 写入T => 消费者(Consumer)=> super

还是以Collections的copy()方法为例:

public class Collections {public static <T> void copy(List<? super T> dest, List<? extends T> src) {for (int i=0; i<src.size(); i++) {T t = src.get(i); // src是producerdest.add(t); // dest是consumer}}
}

需要返回T的src是生产者,因此声明为List<? extends T>,需要写入T的dest是消费者,因此声明为List<? super T>。

6.4.无限定通配符

我们已经讨论了<? extends T>和<? super T>作为方法参数的作用。实际上,Java的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个?:

void sample(Pair<?> p) {
}

因为<?>通配符既没有extends,也没有super,因此:

  • 不允许调用set(T)方法并传入引用(null除外);
  • 不允许调用T get()方法并获取T引用(只能获取Object引用)。

换句话说,既不能读,也不能写,那只能做一些null判断:

static boolean isNull(Pair<?> p) {return p.getFirst() == null || p.getLast() == null;
}

大多数情况下,可以引入泛型参数<T>消除<?>通配符

static <T> boolean isNull(Pair<T> p) {return p.getFirst() == null || p.getLast() == null;
}

<?>通配符有一个独特的特点,就是:Pair<?>是所有Pair<T>的超类

public class Main {public static void main(String[] args) {Pair<Integer> p = new Pair<>(123, 456);Pair<?> p2 = p; // 安全地向上转型System.out.println(p2.getFirst() + ", " + p2.getLast());}}class Pair<T> {private T first;private T last;public Pair(T first, T last) {this.first = first;this.last = last;}public T getFirst() {return first;}public T getLast() {return last;}public void setFirst(T first) {this.first = first;}public void setLast(T last) {this.last = last;}
}

上述代码是可以正常编译运行的,因为Pair<Integer>是Pair<?>的子类,可以安全地向上转型。

6.5.小结

使用类似<? super Integer>通配符作为方法参数时表示:

  • 方法内部可以调用传入Integer引用的方法,例如:obj.setFirst(Integer n);
  • 方法内部无法调用获取Integer引用的方法(Object除外),例如:Integer n = obj.getFirst();

即使用super通配符表示只能写不能读;
使用extends和super通配符要遵循PECS原则;
无限定通配符<?>很少使用,可以用<T>替换,同时它是所有<T>类型的超类。

7.泛型和反射(待深入理解)

7.1.使用泛型的反射API

Java的部分反射API也是泛型。

7.1.1.Class<T>就是泛型

// compile warning:
Class clazz = String.class;
String str = (String) clazz.newInstance();// no warning:
Class<String> clazz = String.class;
String str = clazz.newInstance();

调用Class的getSuperclass()方法返回的Class类型是Class<? super T>:

Class<? super String> sup = String.class.getSuperclass();

7.1.2.构造方法Constructor<T>也是泛型

Class<Integer> clazz = Integer.class;
Constructor<Integer> cons = clazz.getConstructor(int.class);
Integer i = cons.newInstance(123);

7.1.3.可以声明带泛型的数组,但不能用new操作符创建带泛型的数组

Pair<String>[] ps = null; // ok
Pair<String>[] ps = new Pair<String>[2]; // compile error!

必须通过强制转型实现带泛型的数组

@SuppressWarnings("unchecked")
Pair<String>[] ps = (Pair<String>[]) new Pair[2];

使用泛型数组要特别小心,因为数组实际上在运行期没有泛型,编译器可以强制检查变量ps,因为它的类型是泛型数组。但是,编译器不会检查变量arr,因为它不是泛型数组。因为这两个变量实际上指向同一个数组,所以,操作arr可能导致从ps获取元素时报错,例如,以下代码演示了不安全地使用带泛型的数组:

Pair[] arr = new Pair[2];
Pair<String>[] ps = (Pair<String>[]) arr;ps[0] = new Pair<String>("a", "b");
arr[1] = new Pair<Integer>(1, 2);// ClassCastException:
Pair<String> p = ps[1];
String s = p.getFirst();

要安全地使用泛型数组,必须扔掉arr的引用:

@SuppressWarnings("unchecked")
Pair<String>[] ps = (Pair<String>[]) new Pair[2];

上面的代码中,由于拿不到原始数组的引用,就只能对泛型数组ps进行操作,这种操作就是安全的。
带泛型的数组实际上是编译器的类型擦除:

Pair[] arr = new Pair[2];
Pair<String>[] ps = (Pair<String>[]) arr;System.out.println(ps.getClass() == Pair[].class); // trueString s1 = (String) arr[0].getFirst();
String s2 = ps[0].getFirst();

所以我们不能直接创建泛型数组T[],因为擦拭后代码变为Object[]:

// compile error:
public class Abc<T> {T[] createArray() {return new T[5];}
}

必须借助Class<T>来创建泛型数组

T[] createArray(Class<T> cls) {return (T[]) Array.newInstance(cls, 5);
}

我们还可以利用可变参数创建泛型数组T[]

public class ArrayHelper {@SafeVarargsstatic <T> T[] asArray(T... objs) {return objs;}
}String[] ss = ArrayHelper.asArray("a", "b", "c");
Integer[] ns = ArrayHelper.asArray(1, 2, 3);

7.2.谨慎使用泛型可变参数

在上面的例子中,我们看到,通过:

static <T> T[] asArray(T... objs) {return objs;
}

似乎可以安全地创建一个泛型数组。但实际上,这种方法非常危险。以下代码来自《Effective Java》的示例:

import java.util.Arrays;public class Main {public static void main(String[] args) {String[] arr = asArray("one", "two", "three");System.out.println(Arrays.toString(arr));// ClassCastException:String[] firstTwo = pickTwo("one", "two", "three");System.out.println(Arrays.toString(firstTwo));}static <K> K[] pickTwo(K k1, K k2, K k3) {return asArray(k1, k2);}static <T> T[] asArray(T... objs) {return objs;}}

直接调用asArray(T…)似乎没有问题,但是在另一个方法中,我们返回一个泛型数组就会产生ClassCastException,原因还是因为擦拭法,在pickTwo()方法内部,编译器无法检测K[]的正确类型,因此返回了Object[]。
如果仔细观察,可以发现编译器对所有可变泛型参数都会发出警告,除非确认完全没有问题,才可以用@SafeVarargs消除警告。
如果在方法内部创建了泛型数组,最好不要将它返回给外部使用
更详细的解释请参考《Effective Java》“Item 32: Combine generics and varargs judiciously”。

7.3.小结

  • 部分反射API是泛型,例如:Class<T>,Constructor<T>;
  • 可以声明带泛型的数组,但不能直接创建带泛型的数组,必须强制转型;
  • 可以通过Array.newInstance(Class<T>, int)创建T[]数组,需要强制转型;
  • 同时使用泛型和可变参数时需要特别小心。

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

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

相关文章

【廖雪峰官方网站/Java教程】多线程(1)

多线程是Java最基本的一种并发模型&#xff0c;本章我们将详细介绍Java多线程编程。 1.多线程基础 1.1.进程 在计算机中&#xff0c;我们把一个任务称为一个进程&#xff0c;浏览器就是一个进程&#xff0c;视频播放器是另一个进程&#xff0c;类似的&#xff0c;音乐播放器…

【廖雪峰官方网站/Java教程】多线程(2)

1.使用wait和notify 1.1.多线程协调 在Java程序中&#xff0c;synchronized解决了多线程竞争的问题。例如&#xff0c;对于一个任务管理器&#xff0c;多个线程同时往队列中添加任务&#xff0c;可以用synchronized加锁&#xff1a; class TaskQueue {Queue<String> q…

【廖雪峰官方网站/Java教程】多线程(3)

1.使用线程池 1.1.ExecutorService介绍 Java语言虽然内置了多线程支持&#xff0c;启动一个新线程非常方便&#xff0c;但是&#xff0c;创建线程需要操作系统资源&#xff08;线程资源&#xff0c;栈空间等&#xff09;&#xff0c;频繁创建和销毁大量线程需要消耗大量时间。…

【廖雪峰官方网站/Java教程】Maven基础

Maven是一个Java项目管理和构建工具&#xff0c;它可以定义项目结构、项目依赖&#xff0c;并使用统一的方式进行自动化构建&#xff0c;是Java项目不可缺少的工具。 1.Maven介绍 1.1.Maven功能及项目结构 1.1.1.Maven主要功能 Maven就是是专门为Java项目打造的管理和构建工…

【廖雪峰官方网站/Java教程】设计模式(一)

0.概述.设计模式的基本概念及原则 设计模式&#xff0c;即Design Patterns&#xff0c;是指在软件设计中&#xff0c;被反复使用的一种代码设计经验。使用设计模式的目的是为了可重用代码&#xff0c;提高代码的可扩展性和可维护性。 为什么要使用设计模式&#xff1f;根本原因…

[转]上海新东方vs新东方,SEO实战

引用前言&#xff1a;半夜无聊上网&#xff0c;看到了这篇文章&#xff0c;觉得还不错&#xff0c;看了收获不小&#xff0c;所以就转过来了。来源 非常郑重地声明一下&#xff08;文章发表约20小时后补充&#xff09; 我不得不承认&#xff0c;这篇文章有点“软”&#xff0c;…

前端web:响应式网站开发的现状你了解吗?

当企业对网络营销有了更深的认识时&#xff0c;不管是大企业还是小企业&#xff0c;都已经建立了自己的个性化响应网站&#xff0c;都希望利用互联网这一新形态的市场&#xff0c;以及网络营销的新型营销模式。如今建立响应式网站也是一种趋势&#xff0c;利用互联网的优势&…

学用MVC4做网站五:5.4删除文章

前几天把添加、修改功能都做了&#xff0c;今天开始写删除功能。删除文章既要删除文章本身同时也要在公共模型中删除对应项。 首先写从数据库中删除文章的函数。打开ArticleRepository修改Delete的函数。有上次的教训这次明白了传递的id应该是公共模型id。 /// <summary>…

三分钟免费搞定网站在线客服,利用PowerTalkBox控件制作而成,为大家提供比较好的示例...

下载地址:http://download.csdn.net/source/1876659 必须安装.net2.0才可以支持网站服务端 内带完整的安装流程,支持飞信功能,使您不在电脑前时也可以用手机交流. 可以利用以下的js代码实现浮动窗口的功能. <script languagejavascript>var cao_x,cao_y; function cao888…

amazon s3_在Amazon S3上托管静态网站

amazon s3Static website hosting on Amazon S3 is one of the very popular use cases of Amazon S3. It allows you to host an entire static website and on very low cost. Amazon S3 is a highly available and scalable hosting solution.Amazon S3上的静态网站托管是Am…

一个可以实时查相关电子产品价格的网站

香港价格网&#xff0c;里面的价格和香港的百老汇、丰泽等的价格几乎同步&#xff0c;相差不大&#xff0c;有很大的参考价值&#xff0c;对于准备去香港买电子产品的网友来说&#xff0c;是个非常好的网站&#xff0c;特别分享&#xff1a; http://www.price.com.hk/转载于:ht…

简单高效!25个漂亮的简约风格网站设计作品

在过去几年里&#xff0c;网站设计领域发生了巨大变化。除了 RWD&#xff08;响应式网页设计&#xff09;和 Web 字体的革命&#xff0c;现代设计的发展趋势迅速流行扁平化的配色方案&#xff0c;网页排版变得更加重要&#xff0c;重点已放在内容第一。最后&#xff0c;页面加载…

增城seo搜索引擎优化_搜索引擎seo优化主要从哪里入手?

首先我们应该了解什么是搜索引擎优化以及网站搜索引擎seo优化的价值&#xff0c;从基础开始逐步深入&#xff0c;下面拓王朝所要讲的都是一些理论知识&#xff0c;很好理解&#xff0c;有不同见解欢迎评论。SEO优化SEO搜索引擎优化&#xff0c;是指通过采用易于搜索引擎索引和排…

[转]使用ThinkPHP框架快速开发网站(多图)

本文转自&#xff1a;http://blog.csdn.net/ruby97/article/details/7574851 这一周一直忙于做实验室的网站&#xff0c;基本功能算是完成了。比较有收获的是大概了解了ThinkPHP框架。写一些东西留作纪念吧。如果对于同样是Web方面新手的你有一丝丝帮助&#xff0c;那就更好了挖…

《大型网站技术架构》读书笔记[3] - 架构核心五要素

架构设计中要考虑的核心五要素&#xff1b; 性能、可用性、扩展性、伸缩性、安全性 性能 性能的测试指标 响应时间 应用执行一个操作需要的时间&#xff0c;包括从发出请求开始到收到最后响应数据所需要的时间。响应时间是系统最重要的性能指标&#xff0c;直观地反映了系统的“…

java抓取网页数据_Golang丨Java丨Python爬虫实战—Boss直聘网站数据抓取

我们分别通过Golang、Python、Java三门语言&#xff0c;分别实现对Boss直聘网站的招聘数据进行爬取。首先打开Boss直聘网站&#xff1a;然后我们在职位类型中输入Go或者Golang关键字&#xff1a;然后我们可以看到一个列表&#xff0c;和Go语言相关的各种招聘职位&#xff0c;还…

我的网站被黑了,关键词被劫持,总结一下是怎么解决的。

1、发现被黑&#xff0c;网站被黑的症状 两年前自己用wordpress搭了一个网站&#xff0c;平时没事写写文章玩玩。但是前些日子&#xff0c;突然发现网站的流量突然变小&#xff0c;site了一下百度收录&#xff0c;发现出了大问题&#xff0c;网站被黑了。大多数百度抓取收录的页…

一个大图切成几个小图加载速度更快_谷歌SEO页面速度的重要性

什么是页面速度&#xff1f;页面速度是指网页加载所需的时间。一个页面的加载速度是由几个不同的因素决定的&#xff0c;包括网站的服务器、页面文件大小和图片压缩。也就是说&#xff0c;"页面速度 "并不像 "网页速度 "那么重要。"页面速度 "并…

大学计算机思维导图_3款免费在线思维导图网站,你一定要收藏一个!

1&#xff1a;迅捷画图https://www.liuchengtu.com/迅捷画图是一个专业的思维导图、流程图制作网站。支持在线创作流程图、思维导图、组织结构、ER图、网络拓扑图、UML图等等。接下来说说特色&#xff1a;l 支持导出多种格式&#xff0c;如JPG、PNG、PDF文件、txt文本等格式l 提…

python中data.find_all爬取网站为空列表_利用Golang快速爬取盗版网站的整套音频

01前言最近因为 Zigma 帮我写了个推广 Catcher 小程序软文的原因&#xff0c;答应了他帮他爬了一个盗版音频网站的整套 《李淼谈奇案》 。在制作爬虫脚本的过程中&#xff0c;也是遇到了一些有趣的问题&#xff0c;所以特此写了这篇 Blog 用于记录脚本的整一个实现与问题解决。…