泛型理解
Java中泛型由来
Oracle官网中给出如下定义:Generics - This long-awaited enhancement to the type system allows a type or method to operate on objects of various types while providing compile-time type safety. It adds compile-time type safety to the Collections Framework and eliminates the drudgery of casting。翻译过来就是:对于类型系统(type system)这个期待已久的增强功能可以让同一类型或方法在操作不同类型对象的时候,可以保证编译时期类型安全。泛型的出现为集合框架增添了编译时期的类型安全检查功能,并且减少了强制转换的工作。
下面让我们通过例子来进一步了解
上述代码中add()方法可以明确知道我们塞在容器中的值类型是Integer,如果我们想要获取列表中存储的值1,就必须进行显示的强制转换,如果不进行强制转换,IDEA会提醒编译报错不兼容的类型。这时候你有可能会问:明明我塞的是Integer类型的数据,它为什么会提示我从容器中找到的是Object类型,这时候我们只用看一下List源码中的定义就啥都明白了。因为List的存储数据结构是个Object[]数组。
如果我们不想进行显示的强制转换,那么可以写成下面这种方式。而下面这种在一对尖括号中约束类型的方式就是泛型。
泛型的出现让我们在创建容器之初的时候就指定好我们想要存储的数据类型。这样在后续的代码编写过程中,随时都可以知道容器中的数据类型。这样在获取数据的时候就不需要进行强制类型转换。在JDK1.5之前泛型还没有出现,如果图1中第三行代码变为String i = intList.get(0)也是可以编译通过的,但只会在运行的时候报错ClassCastException。这样就导致代码编写过程中,如果因为程序员粗心大意导致类型转换错误,也只有等到程序运行的时候才能发现问题。而泛型的出现让这种错误发生的时间提前到了编译时期,可以说是一个大的飞跃。
泛型在Java中的应用
泛型的本质是参数化类型(Parameterized Type)或者参数化多态(Parametric Polymorphism)的应用,即可以将操作的数据类型指定为方法签名中的一种特殊参数,这种参数类型能够应用在类,接口,方法的创建中,分别构成泛型类,泛型接口,泛型方法。下面让我们通过代码来了解一下。
泛型类演示:
class ThreeBody<T>{ private T t; public T getObj(){ return t; } public void setObj(T t){ this.t = t; } } class YeWenJie{ private String name="叶文洁"; public String getName(){ return name; } } class LuoJi{ private String name="罗辑"; public String getName(){ return name; } } ----------------------------------------------------------- public static void main(String[] args) { ThreeBody<YeWenJie> ye = new ThreeBody<>(); ye.setObj(new YeWenJie()); System.out.println(ye.getObj().getName());//叶文洁 ThreeBody<LuoJi> luoJi = new ThreeBody<>(); luoJi.setObj(new LuoJi()); System.out.println(luoJi.getObj().getName());//罗辑 }
泛型类如何定义:在类名称后面跟上<>,并且在<>中定义一个标识即可。Java中没有明确规定这个标识是什么,但是一般情况下我们是这样约定俗成的:T代表类型,K代表键,V代表值,E代表元素。因为ThreeBody类是用来存放不同类型数据的。所以我们在定义的时候可以用T来做占位符。上述ThreeBody类就是一个泛型类,它可以看作是一个容器,可以根据不同的场景来存放不同类型的实体数据。
泛型接口演示:泛型接口的定义和泛型类的定义没什么区别
public interface Collection<E> extends Iterable<E> { boolean add(E e); boolean remove(Object o); ... }
泛型方法:只有在方法的返回值前加上<>标识,才算真正的泛型方法
public <T> T[] toArray(T[] a) { if (a.length < size) // Make a new array of a's runtime type, but my contents: return (T[]) Arrays.copyOf(elementData, size, a.getClass()); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; }
通配符(Wildcards)
让我们来看两段代码,第一段代码没有使用泛型,它的容器中可以放置多种类型的数据。第二段代码使用了泛型,这就决定了容器中所能盛放的数据类型非常有限。如果我们既想使用泛型,又能支持和第一段代码相同多的数据类型。那就可以使用?标识符。
void printCollection(Collection c){ Iterator iterator = c.iterator(); for(int i = 0; i < c.size(); i++){ System.out.println(iterator.next()); } } --------------------------------------------------- void printCollection(Collection<Integer> c){ for(Integer o : c){ System.out.println(o); } } --------------------------------------------------- void printCollection(Collection<?> c){ for(Object o : c){ System.out.println(o); } }
有界通配符(Bounded Wildcards)有两个关键字extends和super。extends用来表示下界,比如类型范围可以是父亲和儿子。super用来表示上界,比如类型可以是父亲和爷爷。让我们看下下面的代码
abstract class ThreeBody{ abstract void say(); } class YeWenJie extends ThreeBody{ private String name="叶文洁"; public String getName(){ return name; } @Override void say() { System.out.println("首领叶文洁"); } } class LuoJi extends ThreeBody{ private String name="罗辑"; public String getName(){ return name; } @Override void say() { System.out.println("执剑人罗辑"); } }
如果我们定义了一个print方法如下:
当我们想要调用该方法的时候,只能使用下图中第一种调用方法。第二种调用方法会报错。
如果我们想让第二种调用方法可以成功那么这时候就可以使用有界通配符来改造了,改造如下:
经过改造后,只要确保传入的List集合的参数类型是ThreeBody或者ThreeBody的子类即可。如果想要限定传入的参数类型是特定类型或特定类型的超类,则可以使用关键字super。下面我们来看一下super演示:这里的代码虽然不完美,但也可以说明问题。
class AA{} class BB extends AA{} class CC extends BB{} class DD extends BB{}
如果test方法中已经指定了具体的类型,那么只有符合类型的数据不会报错,其他类型的数据会报下面的错误
如果将test修改为下面这种形式,super的含义是指定类或指定类的超类,所以第一和第二个test没有问题,但是第三个和第四个因为不符合要求所以会报错。注意:当使用super关键字时,标记3中的类型BB需要进行修改,否则会报不兼容类型错误。
下面我们来个例子实际感受一下泛型的使用。如果我们想要自定义一个可以支持序列化到文件和从文件反序列化的队列,且这个队列可以支持多种数据类型。那么就可以使用如下的实现方式(伪代码):
public class SerializableConcurrentLinkedQueue<M extends Serializable> implements ShutdownHook.IHandler { private ConcurrentLinkedQueue<M> queue = new ConcurrentLinkedQueue<M>() private String filePath; public void add(M x) { if(x!=null) queue.add(x); } public void add(List<M> x) { if (x != null && x.size() > 0) { queue.addAll(x); } } public M poll() { if (queue.isEmpty()) { return null; } return queue.poll(); } public List<M> pollAll() { if (queue.isEmpty()) { return null; } List<M> res = new Vector<M>(); M m = null; while ((m = queue.poll()) != null) { res.add(m); } return res; } public synchronized void shutdown() { new SerializableFileProxy<M>(this.filePath) { @Override public M get() { return poll(); } @Override public void set(M object) { } }.write(); } public synchronized void setFilePath(String filePath) { this.filePath = filePath; new SerializableFileProxy<M>(this.filePath) { @Override public M get() { return null; } @Override public void set(M object) { add(object); } }.read(); } ... }
修修补补写了两天,限于本人才疏学浅,如有错误之处,希望各位大佬能够指正,小弟定会虚心接受。
参考信息:1.深入理解Java虚拟机第三版
2.https://www.oracle.com/technetwork/cn/articles/java/juneau-generics-2255374-zhs.html
3.https://www.oracle.com/technetwork/java/javase/generics-tutorial-159168.pdf
4.https://blog.****.net/s10461/article/details/53941091
5.https://juejin.im/post/5b614848e51d45355d51f792#heading-12