泛型是Java基础知识的重点,虽然咱们在初学Java的时候,都学过泛型,以为本身掌握对于Java泛型的使用(全是错觉),日后的日子,当咱们深刻去阅读一些框架源码,你就发现了,本身会的只是简单的使用,却看不懂别人的泛型代码是怎么写的,还能够这样,没错,别人写出来的代码那叫艺术,而我......html
Java
语言为何存在着泛型,而像一些动态语言Python
,JavaScipt
却没有泛型的概念?java
缘由是,像Java
,C#
这样的静态编译型的语言,它们在传递参数的时候,参数的类型,必须是明确的,看一个例子,简单编写一个存放int
类型的栈—StackInt
,代码以下:git
public class StackInt { private int maxSize; private int[] items; private int top; public StackInt(int maxSize){ this.maxSize = maxSize; this.items = new int[maxSize]; this.top = -1; } public boolean isFull(){ return this.top == this.maxSize-1; } public boolean isNull(){ return this.top <= -1; } public boolean push(int value){ if(this.isFull()){ return false; } this.items[++this.top] = value; return true; } public int pop(){ if(this.isNull()){ throw new RuntimeException("当前栈中无数据"); } int value = this.items[top]; --top; return value; } }
在这里使用构造函数初始化一个StackInt
对象时,能够传入String
字符串吗?很明显是不行的,咱们要求的是int
类型,传入字符串String
类型,这样在语法检查阶段时会报错的,像Java
这样的静态编译型的语言,参数的类型要求是明确的github
参数不安全:引入泛型,可以在编译阶段找出代码的问题,而不是在运行阶段编程
泛型要求在声明时指定实际数据类型,Java 编译器
在编译时会对泛型代码作强类型检查,并在代码违反类型安全时发出告警。早发现,早治理,把隐患扼杀于摇篮,在编译时发现并修复错误所付出的代价远比在运行时小。数组
避免类型转换:安全
未使用泛型:oracle
List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0); //须要在取出Value的时候进行强制转换
使用泛型:app
List<String> list = new ArrayList<String>(); list.add("hello"); String s = list.get(0); //不须要强制转换
重复编码::经过使用泛型,能够实现通用编码,能够处理不一样类型的集合,而且类型安全且易于阅读。像上面的StackInt
类,咱们不能针对每一个类型去编写对应类型的栈,那样太麻烦了,而泛型的出现就很好的解决了这点框架
在上面的StackInt
类有一些很差的地方,那就是太具体了,不够抽象,不够抽象,那么它的复用性也是不高的,例如,在另外的场景下,我须要的是往栈里存String
类型的字符串,或者是其余类型,那么StackInt
类就作不到了,那么有什么方法可以作到呢?再写一个StackString
类,不可能,那样不得累死。那就只有引入基类Object
了,咱们改进一下代码:
public class StackObject { private int maxSize; private Object[] items; private int top; public StackObject(int maxSize){ this.maxSize = maxSize; this.items = new Object[maxSize]; this.top = -1; } public boolean isFull(){ return this.top == this.maxSize-1; } public boolean isNull(){ return this.top <= -1; } public boolean push(Object value){ if(this.isFull()){ return false; } this.items[++this.top] = value; return true; } public Object pop(){ if(this.isNull()){ throw new RuntimeException("当前栈中无数据"); } Object value = this.items[top]; --top; return value; } }
使用StackObject
能够存储任意类型的数据,那么这样作,又有什么优势和缺点呢?
优势:StackObject
类变得相对抽象了,咱们能够往里面存储任何类型的数据,这样就避免了写一些重复代码
缺点:
一、用Object
表示的对象是比较抽象的,它失去了类型的特色,那么咱们在作一些运算的时候,可能会频繁的拆箱装箱的过程
看上面的例图,咱们理解的认为存放了两个数值,12345
和54321
,将两个进行相加,这是很常见的操做,可是报错了,编译器给咱们的提示是,+
操做运算不能用于两个Object
类型,那么只能对其进行类型转换,这也是咱们上面说到的泛型能解决的问题,咱们须要这样作,int sum = (int)val1 + (int)val2;
,同时在涉及拆箱装箱时,是有必定性能的损耗的,关于拆箱装箱
在这里不做描述,能够参考我写过的随笔—— 深刻理解Java之装箱与拆箱
二、对于咱们push
进去的值,咱们在取出的时候,容易忘记类型转换,或者不记得它的类型,类型转换错误,这在后面的一些业务可能埋下祸根,例以下面这个场景:直到运行时错误才暴露出来,这是不安全的,也是违反软件开发原则的,应该尽早的在编译阶段就发现问题,解决问题
三、使用Object
太过于模糊了,没有具体类型的意义
最好不要用到Object
,由于Object
是一切类型的基类,也就是说他把一些类型的特色给抹除了,好比上面存的数字,对于数字来讲,加法运算就是它的一个特色,可是用了Object
,它就失去了这一特色,失去类型特有的行为
泛型:是被参数化的类或接口,是对类型的约定
class name<T1, T2, ..., Tn> { /* ... */ }
通常将泛型中的类名称为原型,而将 <>
指定的参数称为类型参数,<>
至关于类型的约定,T
就是类型,至关于一个占位符,由咱们在调用时指定
使用泛型改进一下上面StackObject
类,可是,数组和泛型不能很好地结合。你不能实例化具备参数化类型的数组,例以下面的代码是不合格的:
public StackT(int maxSize){ this.maxSize = maxSize; this.items = new T[maxSize]; this.top = -1; }
Java
中不容许直接建立泛型数组,这是由于相比于C++
,C#
的语法,Java
泛型实际上是伪泛型,这点在后面会说到,可是,能够经过建立一个类型擦除的数组,而后转型的方式来建立泛型数组。
private int maxSize; private T[] items; private int top; public StackT(int maxSize){ this.maxSize = maxSize; this.items = (T[]) new Object[maxSize]; this.top = -1; }
实际上,真的须要存储泛型,仍是使用容器更合适,回到原来的代码上,须要知道的是,泛型类型不能是基本类型的,须要是包装类
上面说到了Java
中不容许直接建立泛型数组,事实上,Java
中的泛型咱们是很难通new
的方式去实例化对象,不只仅是实例化对象,甚至是获取T
的真实类型也是很难的,固然经过反射的机制仍是能够获取到的,Java
获取真实类型的方式有 3 种,分别是:
一、类名.class
二、对象.getClass
三、class.forName("全限定类名")
可是,在这里,1
和2
的方式都是作不到的,虽然咱们在外边明确的传入了Integer
类型,new StackT<Integer>(3);
可是在StackT
类,使用T.class
仍是获取不到真实类型的,第 2 种方式的话,并无传入对象,前面也说到是没有办法new
方式实例化的,而经过反射机制是能够作到的,这里不做演示,须要了解的话能够参考 —— Java如何得到泛型类的真实类型、 Java经过反射获取泛型的类型
可是在C#
中的泛型以及C++
的模板,这是很容易作到的,因此说Java
的泛型是伪泛型,Java
并非作不到像C#
同样,而是为了迁就老的JDK
语法所做出的妥协,至于上面为何作不到这样,这就要说到泛型的类型擦除了。
再说类型擦除以前,先说一下泛型接口,和泛型方法吧
接口也能够声明泛型,泛型接口语法形式:
public interface Content<T> { T text(); }
泛型接口有两种实现方式:
public class ContentImpl implements Content<Integer> { private int text; public ContentImpl(int text) { this.text = text; } public static void main(String[] args) { ContentImpl one = new ContentImpl(10); System.out.print(one.text()); } } // Output: // 10
public class ContentImpl<T> implements Content<T> { private T text; public ContentImpl(T text) { this.text = text; } @Override public T text() { return text; } public static void main(String[] args) { ContentImpl<String> two = new ContentImpl<>("ABC"); System.out.print(two.text()); } } // Output: // ABC
泛型方法是引入其本身的类型参数的方法。泛型方法能够是普通方法、静态方法以及构造方法。
泛型方法语法形式以下:
public <T> T func(T obj) {}
是否拥有泛型方法,与其所在的类是不是泛型没有关系。
泛型方法的语法包括一个类型参数列表,在尖括号内,它出如今方法的返回类型以前。对于静态泛型方法,类型参数部分必须出如今方法的返回类型以前。类型参数能被用来声明返回值类型,而且能做为泛型方法获得的实际类型参数的占位符。
使用泛型方法的时候,一般没必要指明类型参数,由于编译器会为咱们找出具体的类型。这称为类型参数推断(type argument inference)。类型推断只对赋值操做有效,其余时候并不起做用。若是将一个泛型方法调用的结果做为参数,传递给另外一个方法,这时编译器并不会执行推断。编译器会认为:调用泛型方法后,其返回值被赋给一个 Object 类型的变量。
public class GenericsMethod { public static <T> void printClass(T obj) { System.out.println(obj.getClass().toString()); } public static void main(String[] args) { printClass("abc"); printClass(10); } } // Output: // class java.lang.String // class java.lang.Integer
泛型方法中也可使用可变参数列表
public class GenericVarargsMethod { public static <T> List<T> makeList(T... args) { List<T> result = new ArrayList<T>(); Collections.addAll(result, args); return result; } public static void main(String[] args) { List<String> ls = makeList("A"); System.out.println(ls); ls = makeList("A", "B", "C"); System.out.println(ls); } } // Output: // [A] // [A, B, C]
事实上,Java的运行大体能够分为两个阶段,编译阶段
,运行阶段
那么对于Java
泛型来讲,当编译阶段事后,泛型 T 是已经被擦除了,因此在运行阶段,它已经丢失了 T 的具体信息,而咱们去实例化一个对象的时候,好比T c = new T();
,它的发生时机是在运行阶段,而在运行阶段,你要new T()
,就须要知道 T 的具体类型,实际上这时候 T
是被替换成Integer
了,而JVM
是不知道T
的类型的,因此是没有办法实例化的。
那么,类型擦除作了什么呢?它作了如下工做:
<>
的内容。好比 T get()
方法声明就变成了 Object get()
;List<String>
就变成了 List
。若有必要,插入类型转换以保持类型安全。让咱们来看一个示例:
import java.util.*; public class ErasedTypeEquivalence { public static void main(String[] args) { Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); System.out.println(c1 == c2); } } /* Output: true */
ArrayList<String>
和 ArrayList<Integer>
应该是不一样的类型。不一样的类型会有不一样的行为。例如,若是尝试向 ArrayList<String>
中放入一个 Integer
,所获得的行为(失败)和 向 ArrayList<Integer>
中放入一个 Integer
所获得的行为(成功)彻底不一样。可是结果输出的是true
,这意味着使用泛型时,任何具体的类型信息都被擦除了,ArrayList<Object>
和 ArrayList<Integer>
在运行时,JVM 将它们视为同一类型class java.util.ArrayList
再用一个例子来对于该谜题的补充:
import java.util.*; class Frob {} class Fnorkle {} class Quark<Q> {} class Particle<POSITION, MOMENTUM> {} public class LostInformation { public static void main(String[] args) { List<Frob> list = new ArrayList<>(); Map<Frob, Fnorkle> map = new HashMap<>(); Quark<Fnorkle> quark = new Quark<>(); Particle<Long, Double> p = new Particle<>(); System.out.println(Arrays.toString(list.getClass().getTypeParameters())); System.out.println(Arrays.toString(map.getClass().getTypeParameters())); System.out.println(Arrays.toString(quark.getClass().getTypeParameters())); System.out.println(Arrays.toString(p.getClass().getTypeParameters())); } } /* Output: [E] [K,V] [Q] [POSITION,MOMENTUM] */
根据 JDK 文档,Class.getTypeParameters() “返回一个 TypeVariable 对象数组,表示泛型声明中声明的类型参数...” 这暗示你能够发现这些参数类型。可是正如上例中输出所示,你只能看到用做参数占位符的标识符,这并不是有用的信息。
残酷的现实是:在泛型代码内部,没法获取任何有关泛型参数类型的信息。
以上两个例子皆出《Java 编程思想》第五版 —— On Java 8
中的例子,本文借助该例子,试图讲清楚Java
泛型是使用类型擦除这里机制实现的,能力不足,有错误的地方,还请指正。关于On Java 8
一书,已在github
上开源,并有热心的伙伴将之翻译成中文,如今给出阅读地址,On Java 8
擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操做中,例如转型、instanceof 操做和 new 表达式。由于全部关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒本身,你只是看起来拥有有关参数的类型信息而已。
考虑以下的代码段:
class Foo<T> { T var; }
看上去当你建立一个 Foo 实例时:
Foo<Cat> f = new Foo<>();
class Foo 中的代码应该知道如今工做于 Cat 之上。泛型语法也在强烈暗示整个类中全部 T 出现的地方都被替换,就像在 C++ 中同样。可是事实并不是如此,当你在编写这个类的代码时,必须提醒本身:“不,这只是一个 Object“。
继承问题
泛型时基于类型擦除实现的,因此,泛型类型没法向上转型。
向上转型是指用子类实例去初始化父类,这是面向对象中多态的重要表现。
Integer
继承了 Object
;ArrayList
继承了 List
;可是 List<Interger>
却并不是继承了 List<Object>
。
这是由于,泛型类并无本身独有的 Class
类对象。好比:并不存在 List<Object>.class
或是 List<Interger>.class
,Java 编译器会将两者都视为 List.class
。
如何解决上面所产生的问题:
其实并不必定要经过new
的方式去实例化,咱们能够经过显式的传入源类,一个Class<T> clazz
的对象来补偿擦除,例如instanceof 操做,在程序中尝试使用 instanceof 将会失败。类型标签可使用动态 isInstance()
,这样改进代码:
public class Improve<T> { //错误方法 public boolean f(Object arg) { // error: illegal generic type for instanceof if (arg instanceof T) { return true; } return false; } //改进方法 Class<T> clazz; public Improve(Class<T> clazz) { this.clazz = clazz; } public boolean f(Object arg) { return kind.isInstance(arg); } }
实例化:
试图在 new T()
是行不通的,部分缘由是因为擦除,部分缘由是编译器没法验证 T 是否具备默认(无参)构造函数。
Java 中的解决方案是传入一个工厂对象,并使用该对象建立新实例。方便的工厂对象只是 Class 对象,所以,若是使用类型标记,则可使用 newInstance()
建立该类型的新对象:
class Improve<T> { Class<T> kind; Improve(Class<T> kind) { this.kind = kind; } public T get(){ try { return kind.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } } } class Employee { @Override public String toString() { return "Employee"; } } public class InstantiateGenericType { public static void main(String[] args) { Improve<Employee> fe = new Improve<>(Employee.class); System.out.println(fe.get()); } } /* Output: Employee */
经过这样改进代码,能够实现建立对象的实例,可是要注意的是,newInstance();
方法调用无参构造函数的,若是传入的类型,没有无参构造的话,是会抛出InstantiationException
异常的。
泛型数组:
泛型数组这部分,咱们在上面说到能够经过建立一个类型擦除的数组,而后转型的方式来建立泛型数组,此次咱们能够经过显式的传入源类的方式来编写StackT
类,解决建立泛型数组的问题,代码以下:
public class StackT<T> { private int maxSize; private T[] items; private int top; public StackT(int maxSize, Class<T> clazz){ this.maxSize = maxSize; this.items = this.createArray(clazz); this.top = -1; } public boolean isFull(){ return this.top == this.maxSize-1; } public boolean isNull(){ return this.top <= -1; } public boolean push(T value){ if(this.isFull()){ return false; } this.items[++this.top] = value; return true; } public T pop(){ if(this.isNull()){ throw new RuntimeException("当前栈中无数据"); } T value = this.items[top]; --top; return value; } private T[] createArray(Class<T> clazz){ T[] array =(T[])Array.newInstance(clazz, this.maxSize); return array; } }
有时您可能但愿限制可在参数化类型中用做类型参数的类型。类型边界
能够对泛型的类型参数设置限制条件。例如,对数字进行操做的方法可能只想接受 Number
或其子类的实例。
要声明有界类型参数,请列出类型参数的名称,而后是 extends
关键字,后跟其限制类或接口。
类型边界的语法形式以下:
<T extends XXX>
示例:
public class GenericsExtendsDemo01 { static <T extends Comparable<T>> T max(T x, T y, T z) { T max = x; // 假设x是初始最大值 if (y.compareTo(max) > 0) { max = y; //y 更大 } if (z.compareTo(max) > 0) { max = z; // 如今 z 更大 } return max; // 返回最大对象 } public static void main(String[] args) { System.out.println(max(3, 4, 5)); System.out.println(max(6.6, 8.8, 7.7)); System.out.println(max("pear", "apple", "orange")); } } // Output: // 5 // 8.8 // pear
示例说明:
上面的示例声明了一个泛型方法,类型参数
T extends Comparable<T>
代表传入方法中的类型必须实现了 Comparable 接口。
类型边界能够设置多个,语法形式以下:
<T extends B1 & B2 & B3>
注意:extends 关键字后面的第一个类型参数能够是类或接口,其余类型参数只能是接口。
通配符是Java
泛型中的一个很是重要的知识点。不少时候,咱们其实不是很理解通配符?
和泛型类型T
区别,容易混淆在一块儿,其实仍是很好理解的,?
和 T
都表示不肯定的类型,区别在于咱们能够对 T
进行操做,可是对 ?
不行,好比以下这种 :
// 能够 T t = operate(); // 不能够 ? car = operate();
可是这个并非咱们混淆的缘由,虽然?
和 T
都表示不肯定的类型,T
一般用于泛型类和泛型方法的定义,?
一般用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。用代码解释一下,回到文章最初说的栈类StackT
,咱们以这个为基础来解释,上面的观点:
public class Why { public static void main(String[] args) { StackT<Integer> stackT = new StackT<>(3, Integer.class); stackT.push(8); StackT<String> stackT1 = new StackT<>(3, String.class); stackT1.push("7"); test(stackT1); } public static void test(StackT stackT){ System.out.println(stackT.pop()); } } // Output: 8
以咱们编写的StackT
类,进行测试,编写一个test
方法,传入参数类型StackT
,上面的程序正常输出字符串"7" ,这没有什么问题,问题在这里失去了泛型的限定,传进去的实参StackT1
,是被咱们限定为StackT<String>
,可是咱们经过编译器能够看到stackT.pop()
出来的对象,并无String
类型的特有方法,也就是说,它实际上是Object
类
那么咱们就须要修改test
方法的形参,改成:
public static void test(StackT<String> stackT){ System.out.println(stackT.pop()); }
这样子就回到了咱们问题的本质来了,将形参修改成StackT<String>
,这起到了泛型的限定做用,可是会出现这样的问题,若是咱们须要向该方法传入StackT<Integer>
类型的对象 stackT
是,由于方法形参限定了StackT<String>
,,这时候就报错了
这个时候就是通配符?
起做用了,将方法形参改成StackT<?>
就能够了,这也就肯定了咱们刚刚的结论,?
通配符一般是用于泛型传参,而不是泛型类的定义。
public static void test(StackT<?> stackT){ System.out.println(stackT.pop()); }
可是这种用法咱们一般也不会去用,由于它仍是失去了类型的特色,即当无界泛型通配符做为形参时,做为调用方,并不限定传递的实际参数类型。可是,在方法内部,泛型类的参数和返回值为泛型的方法,不能使用!
这里,StackT.push
就不能用了,由于我并不知道?
传的是Integer
仍是String
,仍是其余类型,因此是会报错的。
可是咱们有时候是有这样的需求的,咱们在接收泛型栈StackT
做为形参的时候,我想表达一种约束的关系,可是又不像StackT<String>
同样,约束的比较死板,而Java
是面向对象的语言,那么就会有继承的机制,我想要的约束关系是我能接收的泛型栈的类型都是Number
类的派生类,即不会像?
无界通配符同样失去类的特征,又不会像StackT<String>
约束的很死,这就引出了上界通配符的概念。
可使用上界通配符
来缩小类型参数的类型范围。
它的语法形式为:<? extends Number>
public class Why { public static void main(String[] args) { StackT<Integer> stackT = new StackT<>(3, Integer.class); stackT.push(8); StackT<String> stackT1 = new StackT<>(3, String.class); stackT1.push("7"); StackT<Double> stackT2 = new StackT<>(3, Double.class); //经过 test(stackT); test(stackT2); //error test(stackT1); } public static void test(StackT<? extends Number> stackT){ System.out.println(stackT.pop()); } }
这样就实现了一类类型的限定,可是需求变动了,我如今但愿的约束关系是我能接收的泛型栈的类型都是Number
类的父类,或者父类的父类,那么有上界,天然就有下界
下界通配符
将未知类型限制为该类型的特定类型或超类类型。
注意:上界通配符和下界通配符不能同时使用。
它的语法形式为:<? super Number>
public class Why { public static void main(String[] args) { StackT<Number> stackT1 = new StackT<>(3, Number.class); stackT1.push(8); StackT<Double> stackT2 = new StackT<>(3, Double.class); StackT<Object> stackT3 = new StackT<>(3, Object.class); //经过 test(stackT1); test(stackT3); //error test(stackT2); } public static void test(StackT<? super Number> stackT){ System.out.println(stackT.pop()); } }
这样子的话,就确保了咱们的test
方法只接收Number
类型以上的方法。泛型的各类高级语法可能在写业务代码的时候能够规避,可是若是你要去写一些框架的时候,因为你不知道框架的使用者的使用场景,那么掌握泛型的高级语法就颇有用了。
前面,咱们提到:泛型不能向上转型。可是,咱们能够经过使用通配符来向上转型。
public class GenericsWildcardDemo { public static void main(String[] args) { List<Integer> intList = new ArrayList<>(); List<Number> numList = intList; // Error List<? extends Integer> intList2 = new ArrayList<>(); List<? extends Number> numList2 = intList2; // OK } }
通配符边界问题,关于一些更加深刻的解惑能够参考整理的转载的文章——Java泛型解惑之上下通配符
Pair<int, char> p = new Pair<>(8, 'a'); // 编译错误
public static <E> void append(List<E> list) { E elem = new E(); // 编译错误 list.add(elem); }
public class MobileDevice<T> { private static T os; // error // ... }
public static <E> void rtti(List<E> list) { if (list instanceof ArrayList<Integer>) { // 编译错误 // ... } } List<Integer> li = new ArrayList<>(); List<Number> ln = (List<Number>) li; // 编译错误
List<Integer>[] arrayOfLists = new List<Integer>[2]; // 编译错误
// Extends Throwable indirectly class MathException<T> extends Exception { /* ... */ } // 编译错误 // Extends Throwable directly class QueueFullException<T> extends Throwable { /* ... */ // 编译错误 public static <T extends Exception, J> void execute(List<J> jobs) { try { for (J job : jobs) // ... } catch (T e) { // compile-time error // ... } }
public class Example { public void print(Set<String> strSet) { } public void print(Set<Integer> intSet) { } // 编译错误 }
泛型一些约定俗成的命名:
Java泛型解惑之 extends T>和 super T>上下界限
7月的直播课——Java 高级语法—泛型