面试|能说说你理解的泛型吗

这片博文围绕Java语言的泛型特性作了详尽的介绍。主要包括以下几个内容:

  • 泛型的概念和作用,产生背景
  • 泛型作为一种Java语言特性,如何定义泛型
  • 介绍代码层面泛型的表达方式:泛型通配符和有限通配符
  • 介绍泛型方法的定义和使用
  • 泛型使用过程中的注意事项

1.泛型的概念和作用

泛型是JDK在1.5版本引入的一个特性,在某种程度上,泛型的出现简化了我们的代码,在编译阶段保证代码的安全性。《Think in Java》中这样解释泛型:泛型实现了参数化类型的概念,使得类型可以作为参数适用于尽可能多的场景。我们平时写代码都是具体的类型,方法参数要么是基本数据类型要么自定义的类型,这样写有什么不好吗,不是的,要看使用场景。下面给出一个最经典的例子:

public class Client {
	public static void main(String[] args) {
		List list = new ArrayList();
		list.add(new Integer(1));
		list.add("2");
		
		Iterator iterator = list.iterator();
		while (iterator.hasNext()) {
			Object object = iterator.next();
			System.out.println(object);
		}
	}
}

List容器放入一个Integer类型的对象和一个字符串,这样编译器是允许的,但是我在获取容器内元素的时候,元素的类型被擦除了,取出来的都是Object类型(因为Object类型是所有类的父类,所以取出来的内容都是Object类型)。类型的向上转换导致丢失了具体类型信息,这对于我们实际工作非常不友好;但是我们可以通过手动向下类型转换进行类型还原,这种做法虽然可行,但对于我编码人员来说繁杂且不可靠。
如果不想手动向下类型转换,唯一的办法就是在放入的时候做一个限制,即一个容器只放入某一种类型的对象元素,这样我编译器就能确定放入的到底对不对。上面的示例改成下面这个样子:

List<Integer> intList = new ArrayList<Integer>();
intList.add(new Integer(2));
intList.add(new Integer(6));
//intList.add("3");  // 不符合类型要求  编译报错
		
for (int i = 0; i < intList.size(); i++ ) {
	Integer integer = intList.get(i);  // 直接获取Integer
	System.out.println(integer);   // 拆箱
}

这里通过泛型把intList容器限定为只存放Integer类型的对象,所以获取的时候不是Object类型,而是Integer类型。也可以通过下面的方法:

List<String> strList = new ArrayList<String>();

把strList容器限定为只存放String类型的对象,这样编译器就知道该容器不能存放其他类型的对象
像List接口或者ArrayList类在定义时是通过尖括号来实现这样的目的,尖括号内具体放置的类型,在List接口或者ArrayList类定义时并不知晓,而是把放置的类型抽象化,用某种符号来表示,这种做法就是类型参数化,使ArrayList类适用更多的场景。

2.如何定义泛型

我们上面介绍了泛型的基本概念和使用场景,那么在代码层面是如何定义一个泛型的呢
泛型是一个特性,它可以实现类型参数化,只要使用到类的地方都可以拥有这个特性;那么我们可以定义一个类,接口或者方法(因为方法参数可以使用自定义类型)拥有泛型的特性,拥有这种特性的类、接口或者方法我们称之为泛型类泛型接口泛型方法
JDK1.5之后引入了很多泛型类和泛型接口,常见的就是容器接口和容器类:

// 泛型接口
public interface List<E> extends Collection<E>  
public interface Set<E> extends Collection<E> 
public interface Map<K,V>

// 泛型类
public class ArrayList<E> extends AbstractList<E> implements List<E>
public class HashSet<E> extends AbstractSet<E> implements Set<E>
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>

// 泛型方法
void add(int index, E element);  // List接口方法
<T> T[] toArray(T[] a);   // Set接口方法
V put(K key, V value);    // Map接口方法

声明接口或者类时,接口名和类名后尖括号内大写字母“E”代表这个接口或者类拥有泛型这个特性它是参数化类型的一种表达方式;泛型类和泛型接口表明该类可以操作某一种类型的类,这个被操作的类的类型没有确定,而是用一个泛化的字母“E”来表示,当我们实际使用的时候就需要确定该类的类型;如泛型类ArrayList< E>的定义,及它的使用ArrayList< Integer>,ArrayList< String>,此时“E”是Integer类和String类的参数化表达。
另外,参数化类型“E”作用于泛型接口和泛型类的整个声明过程,即泛型类属性的声明,泛型类方法的声明等。所以可以知道,拥有泛型特性的类就会拥有泛型方法和泛型属性。同时注意,泛型类的使用并不会实际影响到泛型类的声明,什么意思呢,就是说定义的ArrayList< Integer>和ArrayList< String>并不影响到ArrayList< E>,可以理解为一种形参和实参。
注意:参数化类型一般用单个的大写字母来表示,JDK中用“E”,"K"或者"V"等来表示参数化类型,尽量避免小写,因为容易跟类中的属性或者变量混淆

3.泛型通配符

这个时候你或许有个疑问,Java中不是所有的类型都继承自Object类吗,参数化类型为什么不直接用父类Object表示
在回答这个问题之前,我们先看下面的示例:

List<Integer> intList = new ArrayList<Integer>();   // a
List<Object> objList = intList;    // b

如果按照常规的Object类是Integer类的父类,所以objList引用可以指向intList引用指向的对象,所以会理所当然的认为没有问题。先别急着下结论,下面简单分析下:
假设没有问题,那么下面的操作也可以:

objList.add(new Object());	  // c	
Integer num = intList.get(0);      // d

objList和intList指向同一块内存,两个引用可以同时操作这块内存,所以通过objList引用往内存中添加一个object对象,再通过intList引用获取该对象,此时获取的是Object类型的对象,而intList引用只能获取Integer类型的对象,这两者是相互矛盾的,所以上面的结论是错误的,b代码不会通过编译。即子类型和父类型的关系,在泛型特性里并不能保证,也就不能用Object类型替代类型参数化”E“了

既然List< Object>不是所有泛型接口List的父类,那么什么是泛型接口List的父类呢

大概是无法表达它的父类,Java中用”?“替代字母"E"来表示,如List是List< E>的父类,Set是Set< E>的父类等。"?"表明类型可以匹配任何类型,称为泛型通配符。所以上面的示例可以改为:

List<Integer> intList = new ArrayList<Integer>();
List<?> objList = intList;

objList引用是intList引用的父类。但问题you出现了,我们无法执行下面语句:

objList.add(new Object());   // 编译报错

按理来说,objList是父类引用,放什么都能接受,我们假设这是正确的,下面简单分析下;
泛型类ArrayList< E>的add方法:

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

add方法参数是类型参数化"E",假如我放进去了没有问题,那么通过objList引用取出来呢,编译器无法知道取出来的元素类型是什么,因为objList的泛型是"?",所以在放入的过程就会编译报错。注意,可以放入null,因为null是所有类型共有的。

这个时候你可能觉得泛型父类List没什么用,因为它并不能放任何东西,别急,往下看。

4.有限制的通配符

现在有一个画布可以画各种形状的图形,用代码表示就是:
形状接口以及各形状类:

public interface Shape {
	void draw(Canvas c);   // canvas为画布类
}
// 圆形
public class Circle implements Shape {
	@Override
	public void draw(Canvas c) {
		System.out.println("circle draw");
	}
}
// 正方形
public class Square implements Shape {
	@Override
	public void draw(Canvas c) {
		System.out.println("Square draw");
	}
}

画布类:

public class Canvas {
	public void draw(Shape shape) {   // 一个画布画单个图形
		shape.draw(this);
	}
	
	public void drawAll(List<Shape> shapes) {   // 一个画布画多个图形
		for (Shape shape : shapes) {
			shape.draw(this);
		}
	}
}

客户端:

public class Client {
	public static void main(String[] args) {
		Canvas canvas = new Canvas();
		List<Shape> shapeList = new ArrayList<Shape>();
		shapeList.add(new Square());
		shapeList.add(new Circle());
		canvas.drawAll(shapeList);   // 编译通过
		
		List<Circle> circleList = new ArrayList<Circle>();
		circleList.add(new Circle());
		canvas.drawAll(circleList);     // 编译报错
		
		canvas.draw(new Circle());   // 编译通过
	}
}

是否发现了什么?没错,drawAll()方法只接受List< Shape>类型的引用,而不接受List< Circle>类型的引用,这与我们前面说的遥相呼应。Circle类型是Shape的子类或者实现类,但泛型类List< Shape>并不是泛型类List< Circle>的父类。
如果我们把drawAll(List< Shape> shapes)改成drawAll(List shapes),很明显在循环便利的时候并不知道取出来的元素是什么类型,所以这种方法也不行。Java里面提供一种有限制的通配符,用下面这种方式表示:

List<? extends Shape>

它表示泛型类的参数类型(? extends Shape)是Shape类的子类,即Shape类以及任何子类和实现类,这才是我们想要表达的内容;这种写法之后,编译不再报错。

注意区分这里提到的三个泛型引用:List, List,List< Shape>

5.泛型方法

上面讲到参数化类型是< Shape>的父类,那下面的代码是否行得通呢?

List<? extends Shape> list = new ArrayList<Shape>();
list.add(new Circle());

很不幸,编译报错。原因很简单,是一个表示范围的未知类型,跟是一样的,只不过范围缩小了;所以,< Shape>即可。
上面介绍了泛型类和泛型接口,下面看看泛型方法。定义一个泛型方法:

public <T> void addList(List<T> list, T t) {
    list.add(t);
}

把类型为T的对象t放到一个集合list中,注意public和void之间有一个,少了它泛型方法则不成立。泛型方法可以自己实践,比较简单。

6.泛型使用的注意事项

A.泛型类在使用过程中类型不会被改变
List<Integer> intList = new ArrayList<Integer>();
List<String> strList = new ArrayList<String>();
System.out.println(intList.getClass() == strList.getClass());   // true

运行时获取引用intList和strList的Class对象,如果你了解反射,那么就能明白两者的Class对象是同一个,所以泛型作为类的一种特性,使用过程中不会影响类的类型。
PS::不了解反射的可以看这篇文章。

B.泛型类的instanceOf
if (intList instanceof List<Integer>) {}   // 编译报错
if (intList instanceof List<?>) {}   // 编译通过

第一点说明,运行时并不会区分参数化类型,即类型的信息在运行时被擦除了,所以第一个判断中检查intList引用是否是一个特定类型的泛型类是没有任何意义的。第二个判断,虽然编译能通过,但也没有实际意义。

7.总结

关于泛型的大部分知识已经学完了,主要有下面几个点:

  • 泛型的概念和作用;
  • 如何在Java代码层面定义体现泛型特性;
  • 通过具体问题引出泛型的通配符和有限通配符;
  • 泛型方法的定义和使用;
  • 泛型的两个注意事项

如果有什么迷惑,一起讨论学习。

你可能感兴趣的:(Java基础面试题,Java基础知识)