java 泛型解析

  • 1、泛型的概述:

1.1 基本概述

  • 泛型的本质就是"参数化类型"。一提到参数,最熟悉的就是定义方法的时候需要形参,调用方法的时候,需要传递实参。那"参数化类型"就是将原来具体的类型参数化
  • 泛型的出现避免了强转的操作,在编译器完成类型转化,也就避免了运行的错误。

1.2 泛型的目的

  • Java泛型也是一种语法糖,在编译阶段完成类型的转换的工作,避免在运行时强制类型转换而出现ClassCastException,类型转化异常。

1.3 实例

JDK 1.5增加了泛型,在很大的程度上方便在集合上的使用。如下面的例子,

  • 不使用泛型:
public static void main(String[] args) {
        List list = new ArrayList();
        list.add(11);
        list.add("ssss");
        for (int i = 0; i < list.size(); i++) {
            System.out.println((String)list.get(i));
        }
    }

因为list类型是Object。所以int,String类型的数据都是可以放入的,也是都可以取出的。但是上述的代码,运行时就会抛出类型转化异常。

  • 使用泛型:
public static void main(String[] args) {
        List<String> list = new ArrayList();
        list.add("hahah");
        list.add("ssss");
        for (int i = 0; i < list.size(); i++) {
            System.out.println((String)list.get(i));
        }
    }
在上述的实例中,我们只能添加String类型的数据,否则编译器会报错。

2、泛型的使用

泛型的三种使用方式:泛型类泛型方法泛型接口

2.1 泛型类

  • 泛型类概述:把泛型定义在类上
  • 定义格式:
public class 类名 <泛型类型1,...> {
    
}
  • 注意事项:泛型类型必须是引用类型(非基本数据类型)

2.2 泛型方法

2.2.1 泛型方法概述

  • 把泛型定义在方法上
  • 定义格式:
public 修饰符 <泛型类型> 返回类型 方法名(泛型类型 变量名) {
    
}

如果你定义了一个泛型(类、接口),那么Java规定,你不能在所有的静态方法、静态初块等所有静态内容中使用泛型的类型参数。例如:

public class A<T> {
	public static void func(T t) {
	//报错,编译不通过
	}
}

如何在静态方法中使用泛型,更一般的问题是,如果类(或者接口)没有定义成泛型,但是就想在其中某几个方法中运用泛型(比如接受一个泛型的参数等),该如何解决?

  • 定义泛型方法就像定义泛型类或接口一样,在定义类名(或者接口名)的时候需要指定我的作用域中谁是泛型参数。例如:public class A<T> { ... }表明在类A的作用域中,T是泛型类型参数。
  • 定义泛型方法,如上所述,其格式是:修饰符 <类型参数列表> 返回类型 方法名(形参列表) { 方法体 }。例如:public static <T, S> int func(List<T> list, Map<Integer, S> map) { ... },其中T和S是泛型类型参数。
  • 泛型方法的定义和普通方法定义不同的地方在于需要在修饰符和返回类型之间加一个泛型类型参数的声明,表明在这个方法作用域中谁才是泛型类型参数
  • 不管是普通的类/接口的泛型定义,还是方法的泛型定义都逃不出两大要素:
    • 明哪些是泛型类型参数;
    • 这些类型参数在哪里使用。

2.2.2 泛型方法类型参数的作用域

  • class A<T> { ... }

    中T的作用域就是整个A;

  • public <T> func(...) { ... }

    中T的作用域就是方法func;

  • 类型参数也存在作用域覆盖的问题,可以在一个泛型模板类/接口中继续定义泛型方法,例如:

class A<T> {
	// A已经是一个泛型类,其类型参数是T
	public static <T> void func(T t) {
	// 再在其中定义一个泛型方法,该方法的类型参数也是T
	}
}
//当上述两个类型参数冲突时,在方法中,方法的T会覆盖类的T,即和普通变量的作用域一样,
内部覆盖外部,外部的同名变量是不可见的。
//除非是一些特殊需求,一定要将局部类型参数和外部类型参数区分开来,避免发生不必要的错误,
因此一般正确的定义方式是这样的:
class A<T> {
	public static <S> void func(S s) {

	}
} 

泛型方法的类型参数可以指定上限,类型上限必须在类型参数声明的地方定义上限,不能在方法参数中定义上限。规定了上限就只能在规定范围内指定类型实参,超出这个范围就会直接编译报错。

  • <T extends X> void func(List<T> list){ ... },正确
  • <T extends X> void func(T t){ ... },正确
  • <T> void func(List<T extends X> list){ ... } ,编译错误

关于上下限,2.7中会更详细的解析。

2.2.3 泛型调用

(1) 显式指定方法的类型参数,类型参数要写在尖括号中并放在方法名之前

例如:

object.<String> func(...)

这样就显式指定了泛型方法的类型参数为String,那么所有出现类型参数T的地方都将替换成String类型。


(2) 隐式地自动推断,不指明泛型参数,编译器根据传入的实参类型自动推断类型参数。

例如:

<T> void func(T t){ ... }

隐式调用object.func("name"),根据"name"的类型String推断出类型参数T的类型是String。


(3) 避免歧义

例如:

<T> void func(T t1, T t2){ ... }

如果这样调用的话object.func("name", 15); 虽然编译不会报错,但是仍然会有很大隐患,T到底应该是String还是Integer存在歧义;


(4) 有些歧义Java是会直接当成编译错误的,即所有和泛型参数有关的歧义,

例如:

<T> void func(List<T> l1, List<T> l2){...}

如果这样调用的话,object.func(new List<String>(), new List<Integer>()); 这里会有歧义,编译器无法知道T到底应该是String还是Integer,这种歧义会直接报错的,编译无法通过。

即泛型方法中,如果类型参数刚好就是泛型参数的类型实参,那么这个类型实参不得有歧义,否则直接编译报错。

2.2.4 泛型方法/类型通配符 二者的差异

关于通配符详细的阐述参见2.7的内容

(1) 你会发现所有能用类型通配符(?)解决的问题都能用泛型方法解决,并且泛型方法可以解决的更好。

  • 类型通配符:void func(List<? extends A> list);
  • 完全可以用泛型方法完美解决:<T extends A> void func(List<T> list);

(2) 两种方法可以达到相同的效果,“?”可以代表范围内任意类型,而T也可以传入范围内的任意类型实参,并且泛型方法更进一步,“?”泛型对象是只读的,而泛型方法里的泛型对象是可修改的,即List<T> list中的list是可修改的。

(3) 两者最明显的区别

  • “?”泛型对象是只读的,不可修改,因为“?”类型是不确定的,可以代表范围内任意类型;
  • 而泛型方法中的泛型参数对象是可修改的,因为类型参数T是确定的(在调用方法时确定),因为T可以用范围内任意类型指定;

读:所谓读是指参数泛型类,泛型只作为该参数类的函数返回类型,那这个函数就是读,List作为参数泛型类,它的get函数就是读。

写:所谓写是指参数泛型类,泛型只作为该参数类的函数入参,那这个函数就是写,List作为参数泛型类,它的add函数就是读。

(4) 适用场景

  • 一般只读就用“?”,要修改就用泛型方法。例如:
public <T> void func(List<T> list, T t) {
	list.add(t);
} 
  • 在多个参数、返回值之间存在类型依赖关系就应该使用泛型方法,否则就应该是通配符“?”。具体就是,如果一个方法的返回值、某些参数的类型依赖另一个参数的类型就应该使用泛型方法,因为被依赖的类型如果是不确定的"?",那么其他元素就无法依赖它。

       例如:

<T> void func(List<? extends T> list, T t);

       即第一个参数依赖第二个参数的类型(第一个参数list的类型参数必须是第二个参数的类型或者其子类)。

  • <T, E extends T> void func(List<T> list1, List<E> list2);
     这里E只在形参中出现了一次(类型参数声明不算),并且没有任何其他东西(方法形参、返回值)依赖它,那么就可以把E规约成“?”。规约结果:
    <T> void func(List<T> l1, List<? extends T> l2);
  • 典型应用,容器赋值方法(Java的API):
    public static <T> void 
    Collections.copy(List<T> dest, List<? extends T> src) { ... }
    从src拷贝到dest,那么dest最好是src的类型或者其父类,因为这样才能类型兼容,并且src只是读取,没必要做修改,因此使用“?”还可以强制避免对src做不必要的修改,增加的安全性。

2.3 泛型接口

  • 泛型接口概述:把泛型定义在接口
  • 定义格式:
public interface 接口名<泛型类型> {
    
}
  • 例如:
/**
 * 泛型接口的定义格式:        修饰符  interface 接口名<数据类型> {}
 */
public interface Inter<T> {
    public abstract void show(T t) ;
}
 
/**
 * 子类是泛型类
 */
public class InterImpl<E> implements Inter<E> {
    @Override
    public void show(E t) {
        System.out.println(t);
    }
}
 
 
Inter<String> inter = new InterImpl<String>() ;
inter.show("hello") ;

Java SDK源码中泛型的使用,下面是List接口和ArrayList类的代码片段。

//定义接口时指定了一个类型形参,该形参名为E
public interface List<E> extends Collection<E> {
   //在该接口里,E可以作为类型使用
   public E get(int index) {}
   public void add(E e) {} 
}
 
//定义类时指定了一个类型形参,该形参名为E
public class ArrayList<E> extends AbstractList<E> implements List<E> {
   //在该类里,E可以作为类型使用
   public void set(E e) {
   .......................
   }
}

2.4 泛型类派生子类

父类派生子类的时候不能在包含类型形参,需要传入具体的类型

  • 错误的方式:
public class A extends Container<K, V> {}
  • 正确的方式:
public class A extends Container<Integer, String> {}
  • 也可以不指定具体的类型,系统就会把K,V形参当成Object类型处理
public class A extends Container {}

2.5 泛型构造器

  • 构造器也是一种方法,所以也就产生了所谓的泛型构造器。
  • 和使用普通方法一样没有区别,一种是显示指定泛型参数,另一种是隐式推断
public class Person {
    public <T> Person(T t) {
        System.out.println(t);
    }    
}
public static void main(String[] args) {
    new Person(22);// 隐式
    new <String> Person("hello");//显示
}
  • 特殊说明:

    • 如果构造器是泛型构造器,同时该类也是一个泛型类的情况下,应该如何使用泛型构造器:因为泛型构造器可以显式指定自己的类型参数(需要用到菱形,放在构造器之前),而泛型类自己的类型实参也需要指定(菱形放在构造器之后),这就同时出现了两个菱形了,这就会有一些小问题,具体用法再这里总结一下。 以下面这个例子为代表
    public class Person<E> {
        public <T> Person(T t) {
            System.out.println(t);
        }
    }
    

    正确用法:

    public static void main(String[] args) {
        Person<String> person = new Person("sss");
    }
    

2.6 高级通配符

2.6.1 <? extends T> 上界通配符

  • 上界通配符顾名思义,<? extends T>表示的是类型的上界【包含自身】,因此通配的参数化类型可能是T或T的子类。

    • 正因为无法确定具体的类型是什么,add方法受限(可以添加null,因为null表示任何类型),但可以从列表中获取元素后赋值给父类型。如上图中的第一个例子,第三个add()操作会受限,原因在于List和List是List<? extends Animal>的子类型。它表示集合中的所有元素都是Animal类型或者其子类List<? extends Animal>。
  • 这就是所谓的上限通配符,使用关键字extends来实现,实例化时,指定类型实参只能是extends后类型的子类或其本身。例如:

    
    //Cat是其子类
        List<? extends Animal> list = new ArrayList<Cat>();
    
    这样就确定集合中元素的类型,虽然不确定具体的类型,但最起码知道其父类。
    然后进行其他操作。

2.6.2 <? super T> 下界通配符

  • 下界通配符<? super T>表示的是参数化类型是T的超类型(包含自身),层层至上,直至Object

     它表示集合中的所有元素都是Cat类型或者其父类
     List <? super Cat>  

    这就是所谓的下限通配符,使用关键字super来实现,实例化时,指定类型实参只能是super后类型的超类或其本身。例如:

    • 编译器无从判断get()返回的对象的类型是什么,因此get()方法受限。但是可以进行add()方法,add()方法可以添加T类型和T类型的子类型,如第二个例子中首先添加了一个Cat类型对象,然后添加了两个Cat子类类型的对象,这种方法是可行的,但是如果添加一个Animal类型的对象,显然将继承的关系弄反了,是不可行的。例如:
//Animal是其父类
List<? super Cat> list = new ArrayList<Animal>();

2.6.3 <?> 无界通配符

  • 任意类型,如果没有明确,那么就是Object以及任意的Java类了
  • 无界通配符用<?>表示,?代表了任何的一种类型,能代表任何一种类型的只有null(Object本身也算是一种类型,但却不能代表任何一种类型,所以List和List的含义是不同的,前者类型是Object,也就是继承树的最上层,而后者的类型完全是未知的)

3、泛型擦除

3.1 概念

编译器编译带类型说明的集合时会去掉类型信息

3.2 验证实例

public class GenericTest {
    public static void main(String[] args) {
        new GenericTest().testType();
    }
 
    public void testType(){
        ArrayList<Integer> collection1 = new ArrayList<Integer>();
        ArrayList<String> collection2= new ArrayList<String>();
        
        System.out.println(collection1.getClass()==collection2.getClass());
        //两者class类型一样,即字节码一致
        
        System.out.println(collection2.getClass().getName());
        //class均为java.util.ArrayList,并无实际类型参数信息
    }
}
  • 分析:
    • 这是因为不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一类处理,在内存中也只占用一块内存空间。从Java泛型这一概念提出的目的来看,其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦除,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
    • 在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。
    • 由于系统中并不会真正生成泛型类,所以instanceof运算符后也不能使用泛型类。

4、泛型与反射

  • 把泛型变量当成方法的参数,利用Method类的getGenericParameterTypes方法来获取泛型的实际类型参数。例子:
public class GenericTest {
 
    public static void main(String[] args) throws Exception {
        getParamType();
    }
    
     /*利用反射获取方法参数的实际参数类型*/
    public static void getParamType() throws NoSuchMethodException{
        Method method = GenericTest.class.getMethod("applyMap",Map.class);
        //获取方法的泛型参数的类型
        Type[] types = method.getGenericParameterTypes();
        System.out.println(types[0]);
        //参数化的类型
        ParameterizedType pType  = (ParameterizedType)types[0];
        //原始类型
        System.out.println(pType.getRawType());
        //实际类型参数
        System.out.println(pType.getActualTypeArguments()[0]);
        System.out.println(pType.getActualTypeArguments()[1]);
    }
 
    /*供测试参数类型的方法*/
    public static void applyMap(Map<Integer,String> map){
 
    }
}
  • 通过反射绕开编译器对泛型的类型限制
public static void main(String[] args) throws Exception {
		//定义一个包含int的链表
		ArrayList<Integer> al = new ArrayList<Integer>();
		al.add(1);
		al.add(2);
		//获取链表的add方法,注意这里是Object.class,如果写int.class会抛出NoSuchMethodException异常
		Method m = al.getClass().getMethod("add", Object.class);
		//调用反射中的add方法加入一个string类型的元素,因为add方法的实际参数是Object
		m.invoke(al, "hello");
		System.out.println(al.get(2));
	}
 

5 泛型的限制

5.1 模糊性错误

  • 对于泛型类User<K,V>而言,声明了两个泛型类参数。在类中根据不同的类型参数重载show方法。
public class User<K, V> {
    
    public void show(K k) { // 报错信息:
'show(K)' clashes with 'show(V)'; both methods have same erasure
        
    }
    public void show(V t) {
 
    }
}

由于泛型擦除,二者本质上都是Obejct类型。方法是一样的,所以编译器会报错。

换一个方式:

public class User<K, V> {
 
    public void show(String k) {
 
    }
    public void show(V t) {
 
    }
}

则可以正常的使用

5.2 不能实例化类型参数

编译器也不知道该创建那种类型的对象

public class User<K, V> {
 
    private K key = new K(); // 报错:
Type parameter 'K' cannot be instantiated directly 
}

5.3 对静态成员的限制

静态方法无法访问类上定义的泛型;如果静态方法操作的类型不确定,必须要将泛型定义在方法上。

如果静态方法要使用泛型的话,必须将静态方法定义成泛型方法

public class User<T> {
 
    //错误
    private static T t;
 
    //错误
    public static T getT() {
        return t;
    }
 
    //正确
    public static <K> void test(K k) {
 
    }
}

5.4 对泛型数组的限制

  • 不能实例化元素类型为泛型参数的数组,但是可以将数组指向类型兼容的数组的引用
public class User<T> {
 
    private T[] values;
 
    public User(T[] values) {
        //错误,不能实例化元素类型为泛型参数的数组
        this.values = new T[5];
        //正确,可以将values 指向类型兼容的数组的引用
        this.values = values;
    }
}

5.5 对泛型异常的限制

泛型类不能扩展 Throwable,意味着不能创建泛型异常类。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值