一文通俗理解为什么需要泛型以及泛型的使用

为什么需要泛型?

public static void main(String[] args) {
    ArrayList list = new ArrayList();
    // 由于集合没有做任何限定,任何类型都可以给其中存放
    list.add("abc");
    list.add("def");
    list.add(5);
    Iterator it = list.iterator();
    while (it.hasNext()) {
        // 需要打印每个字符串的长度,就要把迭代出来的对象转成String类型
        String str = (String) it.next();
        System.out.println(str.length());
    }
}

思考:上面这段代码有什么问题吗?如果有,那么出现问题的原因是什么?

编译不报错,但运行发生了报错。

因为没使用泛型,导致我们想使用的时候,一旦强转就会出现类型转换异常,而在我们工作中,其实很多时候,我们都只需要一个集合容器里面放一个类型,如果有多个类型,我们就分多个容器来存放,那么进行约束存放类型的功能,就叫泛型(Generic)语法,让你在设计API时可以指定类或方法支持泛型,这样我们使用API的时候也变得更为简洁,并得到了编译时期的语法检查

泛型的使用

打开ArrayList的源码, 会发现在定义类的时候, 在类名后面加了个,然后这个E, 在多个方法参数里面都进行了使用, 这个E到底是什么?

class ArrayList<E> {
    public boolean add(E e) { }
    public E get(int index) { }
    ...
}

class ArrayList<String> {
    public boolean add(String e) { }
    public String get(int index) { }
    ...
}

class ArrayList<Integer> {
    public boolean add(Integer e) { }
    public Integer get(int index) { }
    ...
}

我 们 发 现 , 只 要 我 们 在 newArrayList<>()的时候,** 在<>括号里面放什么类型, 那么这个E就是什么类型**,比如我们构建的集合是String类型, 那么 E 就 是 String , 我 们 构 建 的 集 合 是Integer类型, 那么E就是Integer;

当然,这个E是可换成自定义的名字,比如说MyType。

使用泛型定义类

新建一个类,然后在<>里面写一个不存在的类型,这个类型甚至不需要我们创建类,然后为了展示,书写了该属性对应的get和set方法。

public class MyGenericClass<MyType> {
//	private E e;
//
//	public E getE() {
//		return e;
//	}
//
//	public void setE(E e) {
//		this.e = e;
//	}
	
	private MyType myType;

	public MyType getMyType() {
		return myType;
	}

	public void setMyType(MyType myType) {
		this.myType = myType;
	}
	
}

在测试类中,我们创建了2次之前所构建的类对象,两次所传递的泛型类型是不一样的,但是它都能进行接收,并且接收完成之后,也可以通过调用set方法来进行赋值,也能通过调用get方法来进行输出该数据。

public class TestMyGenericClass {
	public static void main(String[] args) {
		//模拟构建ArrayList对象的形式来构建一个MyGenericClass类对象
		MyGenericClass<String> my = new MyGenericClass<String>();
		my.setMyType("张三");
		System.out.println(my.getMyType());
		//再来创建一次其他类型的泛型
		MyGenericClass<Integer> my1 = new MyGenericClass<Integer>();
		my1.setMyType(1001);
		System.out.println(my1.getMyType());
	}
}

使用泛型定义接口

public interface MyGenericInterface<E> {
	void add(E e);
	E getE();
}

定义了一个带泛型的接口之后,这个接口被使用的时候,可以直接在写implements的时候,就把泛型给写死了,代表定义类的时候就确定好了泛型的类型了。

public class MyImp2 implements MyGenericInterface<String>{

	@Override
	public void add(String e) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public String getE() {
		// TODO Auto-generated method stub
		return null;
	}

}

定义了一个带泛型的接口之后,这个接口被使用的时候,也可以在该类上仍然不把泛型的类型给确定,让它在构建该类对象的时候去确定该类型。

public class MyImp1<E> implements MyGenericInterface<E>{
	@Override
	public void add(E e) {
		// TODO Auto-generated method stub
		
	}
	@Override
	public E getE() {
		// TODO Auto-generated method stub
		return null;
	}
}

MyImp2类在使用时无需给出泛型,但MyImp1类在使用时必须指明泛型。

public class TestGenericInterface {
	public static void main(String[] args) {
		MyImp1<Integer> my1 = new MyImp1<Integer>();
		//随着你指定的泛型类型不一样,那么它里面的两个方法参数和返回类型也发生了变化
		my1.add(1001);
		//这个类型的创建,不需要你使用泛型了,因为我们在该类构建的时候,就已经确定了父接口的
		//泛型的类型,所以它的方法都跟着变了,以后就不能发生改变了!
		MyImp2 my2 = new MyImp2();
		my2.add("你好");
	}
}

使用泛型定义方法

在方法的返回值前面如果加上泛型的话,那么参数里面的MyType将会报错,所以,通过泛型在方法上的运用,可以让我们在书写方法重载的时候,变的更加的方便

public class MyGenericMethod {
	//在修饰词的位置写一个泛型的尖括号,里面随便写个类型
	//这样的效果同等于在类或者是接口名后加<>,只不过这样
	//的范围就只局限于当前的方法
	public <E> void show1(E e) {
		System.out.println(e.getClass());
	}
	
	public <E> E show2(E e) {
		return e;
	}
	
	public static void main(String[] args) {
		MyGenericMethod mm = new MyGenericMethod();
		mm.show1(123);
		mm.show1("abc");
		mm.show1(3.14);
		
		System.out.println(mm.show2(123));
	}
}

通配符

然而,如果设 Foo 是 Bar 的一个子类型(子类或者子接口),而 G 是具有泛型声明的类或接口,G并不是 G 的子类型!这一点非常值得注意,因为它与大部分人的习惯认为是不同的。

造成这个原因是因为泛型不存在继承关系

类型通配符的上下限

如果你想限制一个方法,限制其参数的泛型类型时,你有两种方式:

  1. 使用<? extends 类>来约束泛型必须要是该类或者是该类的子类

  2. 使用<? super 类>来约束泛型必须要是该类或者是该类的父类

为什么指定通配符上限的集合不能添加元素?

// 定义一个抽象类 Shape
public abstract class Shape {
    public abstract void draw(Canvas c);
}
// 定义 Shape 的子类 Circle
public class Circle extends Shape {
    // 实现画图方法,以打印字符串来模拟画图方法实现
    public void draw(Canvas c) {
        System.out.println("在画布" + c + "上画一个圆");
    }
}
// 定义 Shape 的子类 Rectangle
public class Rectangle extends Shape {
    // 实现画图方法,以打印字符串来模拟画图方法实现
    public void draw(Canvas c) {
        System.out.println("把一个矩形画在画布" + c + "上");
    }
}
public void addRectangle(List<? extends Shape> shapes) {
    // 下面代码引起编译错误
    shapes.add(0, new Rectangle());
}

《java疯狂讲义》给出的答案是:

与使用普通通配符相似的是,shapes.add() 的第二个参数类型是? extends Shape,它表示 Shape 未知的子类,程序无法确定这个类型是什么,所以无法将任何对象添加到这种集合中。

简而言之,这种指定通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型或其子类),不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型)

我的更通俗理解:List<? extends Shape> shapes传进来的是一个类别Shape本身或者子类(这里我们假设有子类A和B,且A与B之间不存在继承关系)的集合,如果shapes添加子类A的对象,那万一shapes运行时传进来的是子类B的集合,那么"shapes.add(0, new Rectangle());"这行代码是存在问题的;同理shapes添加Shape的对象也是如此。

因此通配符上限的集合,即<? extends Shape>是无法添加的元素,而通配符下限的集合,即<? super Shape>确可以。因为<? super Shape>传进来的都是Shape的父类,而我们添加的元素只要是Shape类别或者Shape的子类就是合法的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十有久诚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值