泛型

什么是泛型

泛型(generics)从表面的意思上来看,就是“广泛的类型”,实际上就是类型参数化
注意两点:1、类型 2、参数。就是把类型作为参数传递给某处需要它的地方。

类型参数化这五个字可以完美的解释泛型的一切,理解了这个关键字泛型==,泛型就很容易理解了。

什么时候需要使用到泛型

我们都知道,参数的特点是未知,可变的。那么同样,当我们不能确定某个类型具体是什么的情况下,我们可以使用泛型,在使用的时候“告诉”编译器,这个类型是什么。核心是提高代码的复用性。

例子:对String、Integer对象进行打印(这里仅仅是模拟,用打印代替更复杂的操作 )

class SoutString{
    private String val;
    SoutString(String val){
        this.val = val;
    }
    public void sout(){
        System.out.println(val);
    }
}

class SoutInteger{
    private Integer val;
    SoutInteger(Integer val){
        this.val = val;
    }
    public void sout(){
        System.out.println(val);
    }
}

public class SoutTest {
    public static void main(String[] args) {
        SoutString s1 = new SoutString("str");
        SoutInteger s2 = new SoutInteger(111);

        s1.sout();
        s2.sout();
    }
}

使用SoutString、SoutInteger就可以很方便的完成。但是Java中有几千个类,每个需要打印呢,再采用这种方法就很麻烦了。即使借助一些辅助工具快速完成,代码依然会臃肿无比。我们可以很明显的看出,这两个类,只有变量的类型不一致,其余的都一样。那么只需要把类型当作参数传递给Sout对象即可,我们可以使用泛型,即参数化类型。

public class Sout<T> {

    private T val;

    Sout(T val){
        this.val = val;
    }
    public void sout(){
        System.out.println(val);
    }

    public static void main(String[] args) {
        Sout<String> s1 = new Sout<>("str");
        Sout<Integer> s2 = new Sout<>(111);

        s1.sout();
        s2.sout();
    }
}

在上述代码中我们使用到了,T表示这是个类型参数,当然我们也可以使用A、B、C等等任意一个字母,但是java中有一些默认的命名的规则,为了保证编码风格,大家还是尽量使用默认规则。

K、V 、T 、E 、N 代表什么

上文提到,java中关于泛型有一些默认的规则。
K - Key(键)
V - Value(值)
T - Type(java类型)
E - Element(在集合中使用,因为集合中存在的是元素)
N - Number(数值类型)

泛型 T 的使用

泛型 T 的使用范围:

  • 类、接口上(泛型类)
  • 方法 (泛型方法)
    定义泛型用T,使用泛型一定要传入具体的类型,不要忘了“类型参数化!!!”,既然是参数,那么就有形参、实参之分。
    T是形参,使用时传入的具体类型就是实参。就像我们定义一个方法,定义方法使用的参数是形参,但是使用时一定要传入具体的值。只不过泛型的形参是类型,方法的形参是对象。
作用在类上
泛型类
// <T>声明这是个泛型类
// 其实从某种意思上说  <> 才是声明这是个泛型,<>括起来的T则是声明类型的范围,
// 在这个例子上,T表明这个类型参数的范围就是 T ,这个还是有点绕人的。下文会详细描述
public class Sout<T> { 

    private T val;

    Sout(T val){
        this.val = val;
    }
    public void sout(){
        System.out.println(val.getClass());
        System.out.println(val);
    }

    public static void main(String[] args) {
        // 定义泛型类Sout时使用了T作为形参,那么使用泛型类Sout时需要指定String作为具体的实参
        // Sout<String> 指定了类型时String,那么在Sout中,所有的T就是String
        Sout<String> s1 = new Sout<>("str");
        // Sout<Integer> 指定了实际类型是Integer,那么在Sout中,所有的T就是Integer
        Sout<Integer> s2 = new Sout<>(111);

        s1.sout();
        s2.sout();
    }
}

在这里插入图片描述

泛型接口
//<T> 声明这是个泛型接口,并且类型的范围就是T
public interface Service<T> {

    public T getT();

    public void setT(T t);
}

// 前面已经提过,声明泛型才用T,使用泛型 就要指定参数的类型,所以在此指定具体的实际类型是String
public class ServiceImpl1 implements Service<String> {
    private String val;

    @Override
    public String getT() {
        System.out.println(val.getClass());
        return this.val;
    }

    @Override
    public void setT(String s) {
        this.val = s;
    }

    public static void main(String[] args) {
        //因为在定义ServiceImpl1时,已经指定了Service接口的T是String类型,所以此处不需要在指定T的类型了
        ServiceImpl1 s1 = new ServiceImpl1();
        s1.setT("str");
        System.out.println(s1.getT());//输出:class java.lang.String   str
    }
}

在定义ServiceImpl1时,我们指定了Service的T是String,但是如果还需要一个ServiceImpl2来保证Service的T是Integer呢?甚至是Double呢?如果在定义ServiceImpl2、ServiceImpl3、ServiceImpl4就违背设计泛型的初衷了。所以泛型同样支持实现类本身也是泛型类。

// 声明泛型类ServiceImpl2用T,使用泛型ServiceImpl2 就要指定T具体的类型了
/*
但是注意,此处还使用到了泛型接口Service,使用泛型接口Service要指定具体的类型。
但是这个类型是由ServiceImpl2确定的,所以ServiceImpl2中的形参T作为Service接口的实参,传递给Service.
*/
/*
把service、ServiceImpl2当作方法就容易理解了。
比如 声明了一个Service方法,我们在使用Service方法是需要传递具体的实参。
然后ServiceImpl2方法内部调用了Service方法。那么ServiceImpl2方法的形参对于service来说,就是实参
 */
public class ServiceImpl2<T> implements Service<T > {
    private T val;

    @Override
    public T getT() {
        System.out.println(val.getClass());
        return this.val;
    }

    @Override
    public void setT(T t) {
        this.val = t;
    }

    public static void main(String[] args) {
    	//String作为具体的类型传递给ServiceImpl2,ServiceImpl2内部会把String再传给Service
        ServiceImpl2<String> s1 = new ServiceImpl2<>();
        ServiceImpl2<Integer> s2 = new ServiceImpl2<>();

        s1.setT("str");
        s2.setT(111);

        System.out.println(s1.getT());//class java.lang.String
        System.out.println(s2.getT());//class java.lang.Integer
    }
}

在这里插入图片描述

作用在方法上
public class TestMethod {
    //同样使用 <T> 表明这是个泛型方法,并且指定类型的范围是T
    public static <T> void show1(){
        System.out.println("str");
    }
    public static <T> void show2(T t){
        System.out.println(t.getClass());
        System.out.println(t);
    }
    //泛型方法的返回值可以和泛型有关哦
    public static <T> T show3(T t){
        System.out.println(t.getClass());
        System.out.println(t);
        return t;
    }
    public static void main(String[] args) {
        show1();
        System.out.println("=====================================================");
        show2("asd");
        System.out.println("=====================================================");
        String qwe = show3("qwe");
        System.out.println(qwe);
    }
}

在这里插入图片描述

泛型的上下界限定符 extends 、super

现在有一个新的需求,以Sout为例

public class Sout<T> {

    private T val;

    Sout(T val){
        this.val = val;
    }
    public void sout(){
        System.out.println(val.getClass());
        System.out.println(val);
    }

    public static void main(String[] args) {
        Sout<String> s1 = new Sout<>("str");
        Sout<Integer> s2 = new Sout<>(111);

        s1.sout();
        s2.sout();
    }
}

我们在使用Sout类时,可以直接指定类型,可以指定String、Integer等等。如果现在有一个新的需求,只允许Number的子类才能使用Sout,该怎么办呢?还记得上文中提到的<T>有两个含义,1、表明这是个泛型类或者泛型接口。2、表明类型的范围是T。
那么如果需要限制类型的范围,直接在 “<>”中间限制类型范围即可。java提供了extends关键字,用于限制类型的范围。比如<T extends Number>表明使用时 一定要指定具体的类型,并且这个类型必须是Number的子类。
上面的代码就可以改为:

public class Sout<T extends Number> {

    private T val;

    Sout(T val){
        this.val = val;
    }
    public void sout(){
        System.out.println(val.getClass());
        System.out.println(val);
    }

    public static void main(String[] args) {
        Sout<String> s1 = new Sout<>("str");//此时此行代码,会报错,报错信息:Type parameter 'java.lang.String' is not within its bound; should extend 'java.lang.Number'
        Sout<Integer> s2 = new Sout<>(111);

        s1.sout();
        s2.sout();
    }
}

同样,如果限制Sout的类型参数必须是Integer的父类,那么使用<T super Integer>即可。

通配符 ?

现在同样有一个新的需求,我们需要一个专门处理泛型类Sout的方法。 要求泛型类Sout的参数类型必须是Number的子类,但是这个方法只针对Integer的子类进行处理。
我们可以使用泛型方法完成这个需求:

public class TestMethod2 {
   // 定义泛型类Sout时,指定了参数类型必须是Number的子类
   static  class Sout<T extends Number> {
        private T val;
        Sout(T val){
            this.val = val;
        }

        public T getVal() {
            return val;
        }
    }

    //show3方法只允许接受泛型类Sout,并且参数类型必须是Integer的子类
    // 只需要在声明泛型的 “ <>标志 " 中限制类型的范围即可
    public static <T extends Integer> void show(Sout<T > sout){
        System.out.println(sout.getVal().getClass());
        System.out.println(sout.getVal());
    }
    public static void main(String[] args) {

        show(new Sout<Integer>(111));
    }
}

那如果不允许使用泛型方法呢?java 提供了通配符(?)完成这个功能。代码如下:

public static void show2(Sout<? extends Integer> sout){
        System.out.println(sout.getVal().getClass());
        System.out.println(sout.getVal());
    }
    public static void main(String[] args) {

        //show(new Sout<Integer>(111));
        show2(new Sout<Integer>(222));
    }

通配符(?)表示任意类型(不确定的java类型)。当我们在使用泛型时无法确定泛型的具体的参数类型,可以使用通配符代替。划重点,1、使用泛型时。2、无法确定。这两点已经表达出了很重要的信息。
第一点,使用通配符的前提时,必须是一个泛型类。通配符的作用范围是变量
第二点,上文就说过,我们想要使用泛型,必须指定具体的类型,但是如果不确定的话,就可以使用通配符。
比如:

public class TestMethod3 {
    // 此处我们无法确定Sout的具体参数类型,这个是动态传递过来的,此时可以用通配符代替
    // 注意:? 虽然代表任意类型,但是这个“任意”的范围也要遵守  在定义泛型类Sout时,指定的范围,即extends Number 
    private Sout<?> sout;

    public void setSout(Sout<?> sout) {
        this.sout = sout;
    }

    public Sout<?> getSout() {
        return sout;
    }

    public static void main(String[] args) {

        TestMethod3 testMethod3 = new TestMethod3();
        testMethod3.setSout(testMethod3.new Sout<Integer>(222) );

        testMethod3.getSout().sout();

    }

     class Sout<T extends Number> {
        private T val;
        Sout(T val){
            this.val = val;
        }

        public T getVal() {
            return val;
        }
        public void sout(){
            System.out.println(getVal().getClass());
            System.out.println(getVal());
        }
    }
}

PECS原则

PESC原则有个大前提,就是这个规则是基于集合的。
首先来看下List的get以及add方法

public interface List<E> extends Collection<E> {
    boolean add(E e)
    E get(int index)
}
/*
当我们使用 List<? extends Number> list时,意味着可以向list添加任意类(只要是Number的子类)
那么,list.add(new Integer(1));此时向List中传入的Integer作为具体的类型。
但是接下来list.add(new Double(1.1)); 这个时候又传入了Double作为具体的类型。与Integer冲突。
所以,编译器不会允许向List<? extends XXX> 形式的集合添加任何元素。-
、

但是从List<? extends XXX>  list形式的集合获取元素时允许的,
因为list中无论是Double还是Integer,都是Number类型。

*/
List<? extends Number> list = new ArrayList<>();

list.add(new Integer(1));//编译报错
list.add(new Double(1.1));//编译报错

Number number = list.get(0);//编译通过

结论: <? extends T>只能从中获取数据,不能添加数据。像一个生成者一样,只从生产者 手中获取东西,这就是 Producer Extends (PE)

/*
当我们使用 List<? super Integer> list时,可以向list添加Integer类的父类。
此时我们传入一个Number类型作为具体的参数,这时,因为java中继承的传递性,凡是Integer的子类,就也一定是Number的子类,所以此时add(Integer的子类)是完全可以的。

但是因为,Integer的父类,父接口也有很多,所以编译器无法确定传入的到底是哪个父类,
所以get对象时,只能使用Object作为返回值。对于读操作而言,非常不合适。
*/
List<? super Integer> list = new ArrayList<>();
        
list.add(new Integer(1));

Object object = list.get(0);

结论:<? super Integer>只能添加数据,不能获取数据(准确的说,只能使用Object接受对象,然而这样丧失了泛型的意义)。像消费者一样,需要你生产对象放进去。这就是 Consumer Super(CS)

public static void main(String[] args) {
       List<Integer> list1 = new ArrayList<>();
       list1.add(1);
       list1.add(2);
       list1.add(3);

       List<Integer> list2 = new ArrayList<>();

       copy(list1,list2);
        System.out.println("======================================");
        System.out.println(Arrays.toString(list2.toArray()));
    }

    public static void copy(List<? extends Integer> list1,List<? super Integer> list2){
        //list1.add(new Integer(1)); 编译报错
        for (Integer integer : list1) {
            list2.add(integer);
            //Object object = list2.get(0);
        }
    }

在这里插入图片描述

T 和 ? 的区别

T表示具体类型,作用在接口、类、方法上
?表示任意类型,作用在变量上
有了泛型T,才能使用 ??的初衷就是无法确定T到底是什么类型,所以才有了?。也就是说T?的前提。只有使用<T>声明了泛型类,后续在使用这个泛型类时,才有机会使用到?

ListList<Object> 的区别

1、编译时,编译器不会对List进行类型安全检查,会对List<Object>这种带有参数的进行类型安全检查
2、可以把任何带参数的类型传递给原始类型List,但却不能把List<Integer>传递给接受 List<Object>的方法,因为会产生编译错误

    public static void show1(List list) { }
    public static void show2(List<Object> list) { }
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();

        show1(list);//编译通过
        show2(list);//编译不通过
    }

List<?>List<Object>的区别

List<?>表示可以存放任意类型。 List<Object>表示传进去的具体类型是Object,不能把List<Integer>传递给接受 List<Object>的方法

public static void show1(List<?> list) { }
    public static void show2(List<Object> list) { }
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();

        show1(list);//编译通过
        show2(list);//编译不通过
    }

类型擦除

泛型实际上是一个语法糖,在java文件编译为class文件的时候,就进行了类型擦除
类型擦除:就是从泛型类中清除类型参数的相关信息,并且在必要的时候进行类型检查类型转换
先看个例子:

// 这是个java文件
public class Sout<T> {
    private T val;

    Sout(T val) {
        this.val = val;
    }

    public void sout() {
        System.out.println(this.val.getClass());
        System.out.println(this.val);
    }

    public T getVal() {
        return this.val;
    }

    public static void test(Sout<? extends Integer> asd) {
        asd.sout();
    }
}
// 这是编译后的class文件
public class Sout
{

	private Object val;

	Sout(Object val)
	{
		this.val = val;
	}

	public void sout()
	{
		System.out.println(val.getClass());
		System.out.println(val);
	}

	public Object getVal()
	{
		return val;
	}

	public static void test(Sout asd)
	{
		asd.sout();
	}
}

我们可以清楚的看到class文件中根本就没有T的影子。这是因为javac进行编译的时候,基于一个最左原则(可确定的 、最顶级的父类型)类型参数T替换掉了。
所谓的最左原则换句话说:就是 如果T有上界(比如 T extends XXX),那么就使用XXX代替类型参数T,否则使用Object来代替T,因为Object是所有类的父类嘛。
因此,会引发一个结果:泛型类实例的class都是同一个class,就是说 编译器只为泛型类生成一份字节码

既然class文件底层都没有泛型参数的信息了,那么相应的,我们使用泛型的java代码在编译的时候也会通过类型转换与检查来去除泛型,看例子:

	// 这是java文件
	public static void main(String[] args) {
        Sout<String> s1 = new Sout<>("haha");
        String val1 = s1.getVal();
        s1.sout();

        Sout<Integer> s2 = new Sout<>(2222);
        Integer val2 = s2.getVal();
        s2.sout();
    }
//这是反编译后的class文件
	public static void main(String args[])
	{
		Sout s1 = new Sout("haha");
		String val1 = (String)s1.getVal();
		s1.sout();
		
		Sout s2 = new Sout(Integer.valueOf(2222));
		Integer val2 = (Integer)s2.getVal();
		s2.sout();
	}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值