《疯狂java讲义》读书笔记(十):泛型

《疯狂java讲义》读书笔记(十):泛型

泛型是怎么来的

​ ​ ​ ​ ​ ​ ​ 如果我想创建一个List集合,专门放学生的姓名,那么这个List的元素应该是String类型的。List里面有个add()方法用于添加元素,可以发现它的相关描述是这样的public abstract boolean add(Object e),该方法的参数类型可以是Object类型的,也就是,我在填加学生姓名的同属,不小心把一个Integer类型的参数加进来,也是完全可以的。

​ ​ ​ ​ ​ ​ ​ 当我们遍历这个List集合,需要用到里面的元素时候,要挨个取出来,取出来的时候需要进行强制类型转换,那Integer类型的参数肯定不能转成String类型,势必会出错。

​ ​ ​ ​ ​ ​ ​ 但是Java5之后,引入了参数化类型的概念,允许程序在创建集合的时候指定集合元素的类型。参数化类型就被称为泛型。

​ ​ ​ ​ ​ ​ ​ 如果想创建一个只保存学生人名(就是String类型的)的List集合,那么我们可以这样写List<String> list-new ArrayList<String>()。在遍历取出元素的时候也不用强制类型转换了,也不用担心Integer类型的参数混进来。

定义泛型接口、类

​​ ​ ​ ​ ​ ​ ​ 泛型在集合里比较常见,举几个常见的例子来说明怎么定义泛型接口。

// 定义接口时制定了一个泛型形参,该形参名为E
public interface List<E> {
    // 在该接口里,E可作为类型使用
    // 下面方法可以使用E作为参数类型
    void add(E x);
    Iterator<E> iterator();
    ...
}
// 定义接口时指定了一个泛型形参,该形参名为E
public interface Iterator<E> {
    // 在该接口里E完全可作为类型使用
    E next();
    boolean hasNext();
    ...
}
// 定义接口时指定了一个泛型形参,该形参名为E
public interface Map<K, V> {
    // 在该接口里K、V完全可作为类型使用
    Set<K, V> keySet();
    V put(K key, V value);
    ...
}

​ ​ ​ ​ ​ ​ ​ 泛型类也比较简单。在刚开始学javaweb的时候,手写分页,也会用到泛型类,那时候还不大明白呢hhhh。

public class Person<T> {
	private String name;
	private int age;
	private T info;

	public Person(){}
	//下面这种就是错误的构造器写法
	//public Person<T>(){}
	public Person(String name, int age){
		System.out.println("有参构造器");
		this.name=name;
		this.age=age;
	}

	public T getInfo() {
		return info;
	}

	public void setInfo(T info) {
		this.info = info;
	}

	public String getName() {
		return name;
	}

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

	public void setAge(int age) {
		this.age = age;
	}

	public int getAge() {
		return age;
	}

	@Override
	public String toString() {
		return "Person{" +
				"name='" + name + '\'' +
				", age=" + age +
				'}';
	}
}

​ ​ ​ ​ ​ ​ ​ 当创建带泛型声明的自定义类,为该类定义构造器的时候,构造器名还是原来的类名,不要增加泛型声明。

从泛型派生子类

​ ​ ​ ​ ​ ​ ​ 当使用泛型接口和将泛型类当作父类的时候,不要再包含类型形参。下面这样的表达就是错误的。

public class Teacher extends Person<T>{}

​ 如果想从Person类中派生一个子类,可以改成下面这样,两种方法都对。调用方法时必须为所有的数据形参传入参数值,与调用方法不同的是,使用类、接口时也可以不为泛型形参传入实际的类型参数。

public class Teacher extends Person<String>
public class Teacher extends Person

并不存在泛型类

​​ ​ ​ ​ ​ ​ ​ 举个例子,如果看到ArrayList<String>,应该意识到系统是不会把它当作泛型类处理的,实际在Java里还是会被当做ArrayList来处理的。

List<String> ex1=new ArrayList<>();
List<Integer> ex2=new ArrayList<>();
System.out.println(ex1.getClass()==ex2.getClass());

​​ ​ ​ ​ ​ ​ ​ 上面这个输出结果是true,因为不管泛型的实际类型参数T是啥,在运行时总有同样的类,在Java里List<String>和List<Integer>都被当作同一个类处理,所以内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。因为不存在泛型类,所以instanceof运算符后面不能使用泛型类。

扫描二维码关注公众号,回复: 8736571 查看本文章

类型通配符

​ ​ ​ ​ ​ ​ ​ 为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?>(意思是元素类型未知的List)。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。例如代码。

public void test(List<?> c) {
    for (int i = 0; i < c.size(); i++) {
        System.out.println(c.get(i));
    }
}

​​ ​ ​ ​ ​ ​ ​ 现在使用任何类型的List来调用它,程序依然可以访问集合c中的元素,其类型是Object,这永远是安全的,因为不管List的真实类型是什么,它包含的都是Object。但这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素加入到其中。例如下代码,将会引起编译错误。

List<?> c = new ArrayList<String>();
// 下面代码引起编译错误
c.add(new Object());

​ ​ ​ ​ ​ ​ ​ 因为程序无法确定c集合中的元素类型,所以不能向其中添加对象。程序可以调用get()方法来返回List<?>集合指定索引处的元素,其返回值是一个未知类型,但可以肯定,它总是一个Object。

通配符上下限

​ ​ ​ ​ ​ ​ ​ 为了表示List集合的所有元素是一个类F的子类,Java泛型提供了被限制的泛型通配符。被限制的泛型通配符表示如下:

List<? extends F>

​ ​ ​ ​ ​ ​ ​ List<? extends F>是受限制通配符的例子,此处的问号(?)代表一个未知类型,但是一定是F类的子类型(也可以是F本身),因此可以把F称为这个通配符的上限(upper bound)。类似地,由于程序无法确定这个受限制的通配符的具体类型,所以不能把F对象或其子类的对象加入这个泛型集合中。例如下代码是错误的。

public void addFs(List<? extends F> fs) {
  // 下面代码引起编译错误
  fs.add(0, new S());
}

​ ​ ​ ​ ​ ​ ​ 这种指定通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型),不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型)。对于更广泛的泛型来说,指定通配符上限就是为了支持类型型变。比如Foo是Bar的子类,这样A就相当于A<? extends Foo>的子类,可以将A赋值给A<? extends Foo>类型的变量,这种型变方法被称为协变。

​ ​ ​ ​ ​ ​ ​ 对于协变的泛型类来说,它只能调用泛型类型作为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型);而不能调用泛型类型作为参数的方法。口诀是:协变只出不进。对于指定通配符上限的类型类,相当于通配符上限是Object。

​​ ​ ​ ​ ​ ​ ​ 通配符下限用<? super 类型>的方式来指定,通配符下限的作用与通配符上限的作用恰好相反。指定通配符的下限就是为了支持类型型变。比如Foo是Bar的子类,当程序需要一个A<? super Bar>变量时,程序可将A、A赋值给A<? super Bar>类型的变量,这种型变方法被称为逆变。

​​ ​ ​ ​ ​ ​ ​ 对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种逆变的泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类),从集合中取元素时只能被当成Object类型处理(编译器无法确定取出的到底是哪个父类的对象)。

泛型方法

​ ​ ​ ​ ​ ​ ​ 事先举个例子说明一个问题,List<String>对象不能被当成List<Object>对象使用,也就是说List<String>不是List<Object>类的子类。

​ ​ ​ ​ ​ ​ ​ 那看下面的代码,test()方法适用于将前一个集合里的元素复制到下一个集合里的。test()方法传入了两个实际参数,一个as一个ao,但是出现了编译问题,这是因为test()方法的声明是这样的Collection<T> from, Collection<T> to,对比as和ao的类型可知,根本和Collection<T> from, Collection<T> to不符,所以编译器无法正确识别T所代表的实际类型。但是不要以为List<String>List<Object>类的子类,想当然觉得应该是可以通过编译的。

public class ErrorTest {
	// 声明一个泛型方法,该泛型方法中带一个T泛型形参
	static <T> void test(Collection<T> from, Collection<T> to) {
		for (T ele : from) {
			to.add(ele);
		}
	}
	public static void main(String[] args) {
		List<Object> ao = new ArrayList<>();
		List<String> as = new ArrayList<>();
		// 下面代码将产生编译错误
		test(as, ao);
	}
}

​​ ​ ​ ​ ​ ​ ​ 虽然List<String>不是List<Object>类的子类,但是String类型确实是Object类型的子类,我们可以将该方法的前一个形参类型改为Collection<? extends T>,这样只要前一个Collection集合里的元素类型是后一个Collection集合里的元素类型的子类即可。

public class RightTest {
	// 声明一个泛型方法,该泛型方法中带一个T泛型形参
	static <T> void test(Collection<? extends T> from, Collection<T> to) {
		for (T ele : from) {
			to.add(ele);
		}
	}
	public static void main(String[] args) {
		List<Object> ao = new ArrayList<>();
		List<String> as = new ArrayList<>();
		// 下面代码完全正常
		test(as, ao);
	}
}

擦除和转换

​ ​ ​ ​ ​ ​ ​ 泛型的擦除就是说,在编译的时候集合中指定了确定的对象类型,但在运行时将集合中可以存储的该对象类型擦除了。

​ ​ ​ ​ ​ ​ ​ 说这个不存在泛型类的时候举了个例子,不知道还记得不,把代码再写一遍。

List<String> ex1=new ArrayList<>();
List<Integer> ex2=new ArrayList<>();
System.out.println(ex1.getClass()==ex2.getClass());
//true

​ ​ ​ ​ ​ ​ ​ 为啥这样呢,原因和擦除有关系,由于List<String>和List<Object>类型被转换为List(List实际上是个接口,ArrayList实现了List接口,所以我们的理解也没啥问题),所以打印true。我们打印下试试看。

public class Test03 {

	public static void main(String args[]) {
		List<String> ex1=new ArrayList<>();
		List<Integer> ex2=new ArrayList<>();
		//true
		System.out.println(ex1.getClass()==ex2.getClass());

		Class exam1=ex1.getClass();
		System.out.println("List<String> class is "+exam1.getName());
		Class exam2=ex1.getClass();
		System.out.println("List<Integer> class is "+exam2.getName());
	}
}

/*
true
List<String> class is java.util.ArrayList
List<Integer> class is java.util.ArrayList
 */

​ ​ ​ ​ ​ ​ ​ 书上举的那种情况说的是,当一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。

​ ​ ​ ​ ​ ​ ​ 比如我们定义一个List类型的List对象,该对象保留了集合元素的类型信息,当把这个对象赋给一个List类型的list后,编译器就会丢失掉List类型的List对象的泛型信息,这是典型的擦除,Java允许直接把List对象赋值给一个List类型的变量,但是如果遍历取出的时候,想把它当作除了String类型外的元素类型取出的时候,就会出错。

​ ​ ​ ​ ​ ​ ​ 关于擦除,在牛客网刷题的时候遇到了相关的题目,所以在看本章的时候特意看了看。

​ ​ ​ ​ ​ ​ ​ 再附上1篇感觉写得不错的博文:https://blog.csdn.net/briblue/article/details/76736356

发布了58 篇原创文章 · 获赞 5 · 访问量 6265

猜你喜欢

转载自blog.csdn.net/weixin_40992982/article/details/104024019