Java泛型
为什么要引入泛型?
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
- 适用于多种数据类型执行相同的代码(代码复用) ```java private static int add(int a, int b) { System.out.println(a + “+” + b + “=” + (a + b)); return a + b; }
private static float add(float a, float b) { System.out.println(a + “+” + b + “=” + (a + b)); return a + b; }
private static double add(double a, double b) {
System.out.println(a + “+” + b + “=” + (a + b));
return a + b;
}
// 上面的方法可以通过泛型进行代码复用为一个方法
private static
-
泛型中的类型在使用时指定,不需要强制类型转换(**类型安全,编译器会检查类型**)
```java
// 比如,不使用泛型时
List list = new ArrayList();
list.add(1);
list.add("a");
list.add(new Date());
// 这样,list中的元素将都是Object类型,在取出list中的元素时需要进行强制类型转换,容易出错。
// 而使用泛型后,可以提供类型的约束,提供编译前检查
List<String> list = new ArrayList();
list.add("a");
list.add(1); // 编译器将报错,编译无法通过
泛型的基本使用
泛型类
class Point<T>{ // 此处可以随便写标识符号,T是type的简称
private T var ; // var的类型由T指定,即:由外部指定
public T getVar(){ // 返回值的类型由外部决定
return var ;
}
public void setVar(T var){ // 设置的类型也由外部决定
this.var = var ;
}
}
// 多元泛型
class Notepad<K,V>{ // 此处指定了两个泛型类型
private K key ; // 此变量的类型由外部决定
private V value ; // 此变量的类型由外部决定
}
泛型接口
interface Info<T>{ // 在接口上定义泛型
public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型
}
class InfoImpl<T> implements Info<T>{ // 定义泛型接口的子类
private T var ; // 定义属性
public InfoImpl(T var){ // 通过构造方法设置属性内容
this.setVar(var) ;
}
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
}
public class GenericsDemo24{
public static void main(String arsg[]){
Info<String> i = null; // 声明接口对象
i = new InfoImpl<String>("汤姆") ; // 通过子类实例化对象
System.out.println("内容:" + i.getVar()) ;
}
}
泛型方法
泛型方法的定义格式
泛型方法的调用格式
Object obj = getObject(Class.forName("java.lang.String"));
system.out.println(obj.getClass()); // java.lang.String
- 定义泛型方法时,必须在返回值前边加一个
<T>
,来声明这是一个泛型方法,持有一个泛型T
,然后才可以用泛型T作为方法的返回值。因为是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用变量c
的newInstance
方法去创建对象,也就是利用反射创建对象。 Class<T>
的作用就是指明泛型的具体类型,而Class<T>
类型的变量c
,可以用来创建泛型类的对象。泛型方法要求的参数是Class<T>
类型,而Class.forName()
方法的返回值也是Class<T>
,因此可以用Class.forName()
作为参数
因为泛型类在实例化的时候就需要指定类型,如果需要换一种类型,就需要重新new对象,而使用泛型方法则可以在调用的再时候指定类型,增加灵活性。
数组的泛型
泛型数组的声明
List<String>[] list11 = new ArrayList<String>[10]; //编译错误,非法创建
List<?>[] list14 = new ArrayList<String>[10]; //编译错误,非法创建
List<String>[] list12 = new ArrayList<?>[10]; //编译错误,需要强转类型
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10]; //OK,但是会有警告
List<String>[] list6 = new ArrayList[10]; //OK,但是会有警告
List<?>[] list15 = new ArrayList<?>[10]; //OK
使用场景
public class GenericsDemo30{
public static void main(String args[]){
Integer i[] = fun1(1,2,3,4,5,6) ; // 返回泛型数组
fun2(i) ;
}
public static <T> T[] fun1(T...arg){ // 接收可变参数
return arg ; // 返回泛型数组
}
public static <T> void fun2(T param[]){ // 输出
System.out.print("接收泛型数组:") ;
for(T t:param){
System.out.print(t + "、") ;
}
}
}
public ArrayWithTypeToken(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size);
}
泛型的上下限
<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
private <E extends Comparable<? super E>> E max(List<? extends E> e1) {
if (e1 == null){
return null;
}
//迭代器返回的元素属于 E 的某个子类型
Iterator<? extends E> iterator = e1.iterator();
E result = iterator.next();
while (iterator.hasNext()){
E next = iterator.next();
if (next.compareTo(result) > 0){
result = next;
}
}
return result;
}
上述代码中的类型参数 E 的范围是<E extends Comparable<? super E>>
,我们可以分步查看:
- 要进行比较,所以 E 需要是可比较的类,因此需要
extends Comparable<…>
(注意这里不要和继承的extends
搞混了,不一样) Comparable< ? super E>
要对 E 进行比较,即 E 的消费者,所以需要用 super- 而参数
List< ? extends E>
表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大
使用多个限制时,用&符号
public class Client {
// 工资低于2500元的上斑族并且站立的乘客车票打8折
public static <T extends Staff/*站客*/ & Passenger/*上班族*/> void discount(T t){
if(t.getSalary()<2500 && t.isStanding()){
System.out.println("恭喜你!您的车票打八折!");
}
}
public static void main(String[] args) {
discount(new Me());
}
}
泛型深入理解
如何理解Java中的泛型是伪泛型?泛型中类型擦除?
泛型的类型擦除原则是:
- 消除类型参数声明,即删除
<>
及其包围的部分。 - 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。比如形如
<T extends Number>
和<? extends Number>
的类型参数被替换为Number
,<? super Number>
被替换为Object。方法的泛型擦除和类定义的泛型参数一样。 - 为了保证类型安全,必要时插入强制类型转换代码。
- 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。
怎么擦除泛型:
[http://softlab.sdut.edu.cn/blog/subaochen/2017/01/generics-type-erasure
如何证明类型的擦除呢?
只需要证明在泛型被编译后的类型是否都为Object或者替换的类型即可。
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<String>();
list1.add("abc"); // String
ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(123); // Integer
System.out.println(list1.getClass() == list2.getClass()); // true
}
}
public class Test {
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1); // 这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
// 反射获取的是编译后的方法,此时泛型也被擦除
list.getClass().getMethod("add", Object.class).invoke(list, "String");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i)); // 可获取String
}
}
}
如何理解类型擦除后保留的原始类型?
原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
// 擦除泛型后的原始类型
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
如何理解泛型的编译期检查?
Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList();
list1.add("1"); //编译通过
list1.add(1); //编译错误
String str1 = list1.get(0); //返回类型就是String
ArrayList list2 = new ArrayList<String>();
list2.add("1"); //编译通过
list2.add(1); //编译通过
Object object = list2.get(0); //返回类型就是Object
new ArrayList<String>().add("11"); //编译通过
new ArrayList<String>().add(22); //编译错误
String str2 = new ArrayList<String>().get(0); //返回类型就是String
}
}
因为类型检查就是编译时完成的,new ArrayList()只是在内存中开辟了一个存储空间,可以存储任何类型对象,而真正设计类型检查的是它的引用,因为我们是使用它引用list1来调用它的方法,比如说调用add方法,所以list1引用能完成泛型类型的检查。而引用list2没有使用泛型,不能完成泛型检查。
通过上面的例子,可以明白,类型检查就是针对引用的,谁是一个引用(list1,list2),用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象(new ArrayList(),new ArrayList())。
如何理解泛型的多态?泛型的桥接方法
如何理解基本类型不能作为泛型类型?
泛型被擦除后的原始类型都是引用类型,而基本类型不是引用类型,只能使用基本类型的包装类型。
如何理解泛型类型不能实例化?
T test = new T(); // ERROR
Java在编译器不能确定泛型的具体类型,所以在new 对象的时候无法找到具体的类文件,无法通过编译。并且T类型会被擦除为Object,new Object()不本意,如果要实例化泛型,可以通过反射的方式。
static <T> T newTclass (Class < T > clazz) throws InstantiationException, IllegalAccessException {
T obj = clazz.newInstance();
return obj;
}
泛型数组:能不能采用具体的泛型类型进行初始化?
看这么一段代码:
List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa; // lsa 赋值给Object
Object[] oa = (Object[]) o; // Object强转为Object[]
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error ClassCastException.
由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1]
赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException
,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,这是违背泛型原则的。
泛型数组:如何正确的初始化泛型数组实例?
这个无论我们通过
new ArrayList[10]
的形式还是通过泛型通配符的形式初始化泛型数组实例都是存在警告的,也就是说仅仅语法合格,运行时潜在的风险需要我们自己来承担,因此那些方式初始化泛型数组都不是最优雅的方式。
在使用到泛型数组的场景下应该尽量使用列表集合替换,此外也可以通过使用 java.lang.reflect.Array.newInstance(Class<T> componentType, int length)
方法来创建一个具有指定类型和维度的数组,如下:
public class ArrayWithTypeToken<T> {
private T[] array;
public ArrayWithTypeToken(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size); // 使用Array类的newInstance方法反射获取数组对象
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] create() {
return array;
}
}
//...
ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100);
Integer[] array = arrayToken.create();
如何理解泛型类中的静态方法和静态变量?
泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数
因为静态属性是属于类的,在类加载就存在了,不需要实例化类,而泛型参数在类没有实例化时是无法确定类型的,因此静态变量和方法不能使用泛型类所声明的泛型类型参数。
如何理解异常中使用泛型?
- 不能抛出也不能捕获泛型类的对象
```java
public class Problem
extends Exception { // 编译不通过 }
try{
// todo
} catch(Problem
-
不能再catch子句中使用泛型变量
```java
// 泛型信息在编译的时候已经变为原始类型,也就是说上面的T会变为原始类型Throwable,那么如果可以再catch子句中使用泛型变量
public static <T extends Throwable> void doWork(Class<T> t){
try {
} catch(T e) { //编译错误
} catch(IndexOutOfBounds e) {
}
}
// 根据异常捕获的原则,一定是子类在前面,父类在后面,那么上面就违背了这个原则
// 为了避免这样的情况,禁止在catch子句中使用泛型变量。
- 在异常声明中可以使用类型变量
// 下面的使用是合法的
public static<T extends Throwable>/*必须上界为异常体系类*/ void doWork(T t) throws T {
try{
...
} catch(Throwable realCause) {
t.initCause(realCause);
throw t; // 抛出实际异常类
}
}
如何获取泛型的参数类型?
通过反射的方可以获取泛型。
public class GenericType<T> {
private T data;
public static void main(String[] args) {
GenericType<String> genericType = new GenericType<String>() {};
Type superclass = genericType.getClass().getGenericSuperclass();
//getActualTypeArguments 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]
Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
System.out.println(type);//class java.lang.String
}
}
public interface ParameterizedType extends Type {
// 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]
Type[] getActualTypeArguments();
//返回当前class或interface声明的类型, 如List<?>返回List
Type getRawType();
//返回所属类型. 如,当前类型为O<T>.I<S>, 则返回O<T>. 顶级类型将返回null
Type getOwnerType();
}