JAVA基础知识|泛型

痞子三分冷 提交于 2019-11-30 08:24:20

一、什么是泛型?

泛型,即“参数化类型”。

比如定义一个变量A,我们可以通过如下方式将这个变量定义为字符串类型或者整形。

String A;
Integer A;

当然这是在变量类型已知的情况下,如果有一种情况,我们在定义变量的时候不知道以后会需要什么类型,或者说我们需要兼容各种类型的时候,又该如何定义呢?

鉴于以上这种情况,我们可以引入“泛型”,将String和Integer类型进行参数化。在使用的时候,再传入具体的参数类型。

泛型的本质是为了参数化类型(通过传入的不同类型来决定形参的具体类型)。也就是说在泛型使用过程中,数据的类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

 二、泛型类

Computer类,这个类中包含一个属性t,类型为String。

public class Computer {
   private String t;

   public void set(String t) {
      this.t = t;
   }

   public String get() {
      return this.t;
   }
}

泛型类Computer

//这里的"T"并不是固定写法,也可以用V或M等字符
public class Computer<T> {

	private T t;

	public void set(T t) {
		this.t = t;
	}

	public T get() {
		return this.t;
	}
}

//可以传入多种泛型参数
public class Computer<T, V> {

	private T t;
	private V v;

	public void set(T t, V v) {
		this.t = t;
		this.v = v;
	}

	public T getT() {
		return this.t;
	}

	public V getV() {
		return this.v;
	}
}

定义泛型类的好处就是,我们可以在需要的时候,再去指定属性t的类型,增强了通用性。

Computer<String> computer1= new Computer<String>();
Computer<Integer> computer2= new Computer<Integer>();
Computer<Double> computer3= new Computer<Double>();

 

三、泛型接口

//定义接口Calculatable
public interface Calculatable<T> {
    T execute();
}

//传入String类型的实参
public class Computer implements Calculatable<String> {

    @Override
    public String execute() {
        return "ok";
    }
}

//传入Integer类型的实参
public class Calculator implements Calculatable<Integer> {

    @Override
    public Integer execute() {
        return 100;
    }
}

//未传入具体实参,继续抛出,由下层传入
public class Phone<V> implements Calculatable<V> {

    private V v;

    @Override
    public V execute() {
        return this.v;
    }
}

 

四、泛型方法

public class Computer<T> {

    private T t;

    //不是泛型方法
    public void set(T t) {
        this.t = t;
    }

    //不是泛型方法
    public T get() {
        return this.t;
    }

    //泛型方法
    //首先public与返回值类型之间的<V>必不可少,只有声明了<V>的方法才是泛型方法
    //可以声明多个泛型,如 public <V,M> genericMethod(V v,M m)
    public <V> V genericMethod(V v){
        return v;
    }
}

 

Computer<String> computer = new Computer<String>();
Integer v= 100;
System.out.println(computer.genericMethod(v).getClass().toString());

Double v2= 100d;
System.out.println(computer.genericMethod(v2).getClass().toString());

输出结果:

class java.lang.Integer
class java.lang.Double

 

泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型。

泛型方法,可以通过传入的实参,判断V的具体类型。

 

五、静态方法与泛型

 如果静态方法不可以使用类中定义的泛型,如果静态方法想要使用泛型,需要将自身声明为泛型方法。

public class Computer<T> {    
    public static <V> void get(V v){
        //T t;报错,不能使用类中定义的泛型
    }
}

 

六、通配符及上下边界

举一个简单的例子

//水果类
public class Fruit {
    private String name;

    public Fruit(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

fsadfdsfd

//苹果类
public class Apple extends Fruit {
    public Apple(String name) {
        super(name);
    }
}
//装水果的袋子
public class GenericHolder<T> {
    private T obj;

    public GenericHolder() {
    }

    public GenericHolder(T obj) {
        this.obj = obj;
    }

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }
}

测试类:

public class AppTest extends TestCase {

    @Test
    public void test() {

        //这是一个贴了水果标签的袋子贴了
        GenericHolder<Fruit> fruitHolder = new GenericHolder<Fruit>();
        //这是一个贴了苹果标签的袋子
        GenericHolder<Apple> appHolder = new GenericHolder<Apple>();
        //这是一个水果
        Fruit fruit = new Fruit("水果");
        //这是一个苹果
        Apple apple = new Apple("苹果");

        //现在我们把水果放进去
        fruitHolder.setObj(fruit);
        //调用一下吃水果的方法
        eatFruit(fruitHolder);
      
       
        //贴了水果标签的袋子放水果当然没有问题
        //现在我们把水果的子类——苹果放到这个袋子里看看
        fruitHolder.setObj(apple);
        //同样是可以的,其实这时候会发生自动向上转型,apple向上转型为Fruit类型后再传入fruitHolder中
        //但不能再将取出来的对象赋值给redApple了,因为袋子的标签是水果,所以取出来的对象只能赋值给水果类的变量
        //无法通过编译检测 redApple = fruitHolder.getObj();
        //调用一下吃水果的方法
        eatFruit(fruitHolder);

        //放苹果的标签,自然只能放苹果
        appHolder.setObj(apple);
        //报错,这时候无法把appHolder传入eatFruit,因为GenericHolder<Fruit> 和 GenericHolder<Apple>是两种不同的类型,GenericHolder<Fruit>只允许传入水果类的袋子
        // eatFruit(appHolder);
    }

    public static void eatFruit(GenericHolder<Fruit> fruitHolder){
        System.out.println("我正在吃 " + fruitHolder.getObj().getName());
    }
}

执行结果:

我正在吃 水果
我正在吃 苹果

GenericHolder<Fruit> 和 GenericHolder<Apple>是两种不同的类型,所以无法通过编译。

从Java继承的角度上可以分析:

 苹果 IS-A 水果

装苹果的袋子 NOT-IS-A 装水果的袋子

那么问题来了,如果我想让eatFruit方法能同时处理GenericHolder<Fruit> 和 GenericHolder<Apple>两种类型怎么办?而且这也是很合理的需求,毕竟Apple是Fruit的子类,能吃水果,为啥不能吃苹果???如果要把这个方法重载一次,未免也有些小题大做了。

这个时候,泛型的边界符就有它的用武之地了。我们先来看效果:

public class AppTest extends TestCase {

    @Test
    public void test() {

        //这是一个贴了水果标签的袋子
	        GenericHolder<Fruit> fruitHolder = new GenericHolder<Fruit>();
	        //这是一个贴了苹果标签的袋子
	        GenericHolder<Apple> appHolder = new GenericHolder<Apple>();
	        //这是一个水果
	        Fruit fruit = new Fruit("水果");
	        //这是一个苹果
	        Apple apple = new Apple("苹果");

	        //现在我们把水果放进去
	        fruitHolder.setObj(fruit);
	        //调用一下吃水果的方法
	        eatFruit(fruitHolder);

	        //放苹果的标签,自然只能放苹果
	        appHolder.setObj(apple);
	        // 这时候可以顺利把appHolder 传入eatFruit
	        eatFruit(appHolder);
	        //这种泛型 ? extends Fruit不能存放Fruit,但是可以使用 ? extends Fruit的变量指向GenericHolder<Fruit>的对象
	        GenericHolder<? extends Fruit> fruitHolder22 = fruitHolder;
//	        fruitHolder22.setObj(fruit);//报错,不能存放fruit
	        System.out.println(fruitHolder22.getObj().getName()+"----");
	        
	        //这种泛型 ? extends Fruit不能存放apple,但是可以使用 ? extends Fruit的变量指向GenericHolder<apple>的对象
	        fruitHolder22 = appHolder;
//	        fruitHolder22.setObj(apple);//报错不能存放apple
	        System.out.println(fruitHolder22.getObj().getName()+"^^^^");
    }

    public static void eatFruit(GenericHolder<? extends Fruit> fruitHolder){
        System.out.println("我正在吃 " + fruitHolder.getObj().getName());
        Fruit fruit1 = new Fruit("水果1");
        //报错,不能存入fruit1 。因为引入传入的参数fruitHolder是Fruit的子类。
        //Error:(35, 28) java: 不兼容的类型: Fruit无法转换为capture#1, 共 ? extends Fruit
        //fruitHolder.setObj(fruit1);
    }
}

运行结果:

我正在吃 水果
我正在吃 苹果
水果----
苹果^^^^

这就是泛型的边界符,用<? extends Fruit>的形式表示。边界符的意思,自然就是定义一个边界,这里用?表示传入的泛型类型不是固定类型,而是符合规则范围的所有类型,用extends关键字定义了一个上边界。

有上边界,自然有下边界,泛型里使用形如<? super Fruit>的方式使用下边界。

这两种方式基本上解决了我们之前的问题,但是同时,也有一定的限制(PECS原则)。

1.上界<? extends T>不能往里存,只能往外取

不要太疑惑,其实很好理解,因为编译器只知道容器里的是Fruit或者Fruit的子类,但不知道它具体是什么类型,所以存的时候,无法判断是否要存入的数据的类型与容器种的类型一致,所以会拒绝set操作。注意:取出来的是实际的类型,例如上面指向的是GenericHolder<Fruit>,那么取出的就是Fruit,如果指向的是GenericHolder<Apple>,那么取出的就是Apple。

2.下界<? super T>往外取只能赋值给Object变量,不影响往里存

因为编译器只知道它是Fruit或者它的父类,这样实际上是放松了类型限制,Fruit的父类一直到Object类型的对象都可以往里存,但是取的时候,就只能当成Object对象使用了。

3.如果既要存又要取,那么就不要使用任何通配符

所以如果需要经常往外读,则使用<? extends T>,如果需要经常往里存,则使用<? super T>。

 如何阅读过一些Java集合类的源码,可以发现通常我们会将两者结合起来一起用,比如像下面这样:

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

 这种就我看来,应该是相当于:

public class Collections {
    public static <T> void copy(List<T> dest, List<T> src) {
        for (int i=0; i<src.size(); i++)
            dest.set(i, src.get(i));
    }
}

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!