文章目录
什么是泛型
泛型(generics)从表面的意思上来看,就是“广泛的类型”,实际上就是类型参数化。
注意两点:1、类型 2、参数。就是把类型作为参数传递给某处需要它的地方。
类型参数化这五个字可以完美的解释泛型的一切,理解了这个关键字泛型==,泛型就很容易理解了。
什么时候需要使用到泛型
我们都知道,参数的特点是未知,可变的。那么同样,当我们不能确定某个类型具体是什么的情况下,我们可以使用泛型,在使用的时候“告诉”编译器,这个类型是什么。核心是提高代码的复用性。
例子:对String、Integer对象进行打印(这里仅仅是模拟,用打印代替更复杂的操作 )
class SoutString{
private String val;
SoutString(String val){
this.val = val;
}
public void sout(){
System.out.println(val);
}
}
class SoutInteger{
private Integer val;
SoutInteger(Integer val){
this.val = val;
}
public void sout(){
System.out.println(val);
}
}
public class SoutTest {
public static void main(String[] args) {
SoutString s1 = new SoutString("str");
SoutInteger s2 = new SoutInteger(111);
s1.sout();
s2.sout();
}
}
使用SoutString、SoutInteger就可以很方便的完成。但是Java中有几千个类,每个需要打印呢,再采用这种方法就很麻烦了。即使借助一些辅助工具快速完成,代码依然会臃肿无比。我们可以很明显的看出,这两个类,只有变量的类型不一致,其余的都一样。那么只需要把类型当作参数传递给Sout对象即可,我们可以使用泛型,即参数化类型。
public class Sout<T> {
private T val;
Sout(T val){
this.val = val;
}
public void sout(){
System.out.println(val);
}
public static void main(String[] args) {
Sout<String> s1 = new Sout<>("str");
Sout<Integer> s2 = new Sout<>(111);
s1.sout();
s2.sout();
}
}
在上述代码中我们使用到了,T表示这是个类型参数,当然我们也可以使用A、B、C等等任意一个字母,但是java中有一些默认的命名的规则,为了保证编码风格,大家还是尽量使用默认规则。
K、V 、T 、E 、N 代表什么
上文提到,java中关于泛型有一些默认的规则。
K - Key(键)
V - Value(值)
T - Type(java类型)
E - Element(在集合中使用,因为集合中存在的是元素)
N - Number(数值类型)
泛型 T 的使用
泛型 T 的使用范围:
- 类、接口上(泛型类)
- 方法 (泛型方法)
定义泛型用T,使用泛型一定要传入具体的类型,不要忘了“类型参数化!!!”,既然是参数,那么就有形参、实参之分。
T是形参,使用时传入的具体类型就是实参。就像我们定义一个方法,定义方法使用的参数是形参,但是使用时一定要传入具体的值。只不过泛型的形参是类型,方法的形参是对象。
作用在类上
泛型类
// <T>声明这是个泛型类
// 其实从某种意思上说 <> 才是声明这是个泛型,<>括起来的T则是声明类型的范围,
// 在这个例子上,T表明这个类型参数的范围就是 T ,这个还是有点绕人的。下文会详细描述
public class Sout<T> {
private T val;
Sout(T val){
this.val = val;
}
public void sout(){
System.out.println(val.getClass());
System.out.println(val);
}
public static void main(String[] args) {
// 定义泛型类Sout时使用了T作为形参,那么使用泛型类Sout时需要指定String作为具体的实参
// Sout<String> 指定了类型时String,那么在Sout中,所有的T就是String
Sout<String> s1 = new Sout<>("str");
// Sout<Integer> 指定了实际类型是Integer,那么在Sout中,所有的T就是Integer
Sout<Integer> s2 = new Sout<>(111);
s1.sout();
s2.sout();
}
}
泛型接口
//<T> 声明这是个泛型接口,并且类型的范围就是T
public interface Service<T> {
public T getT();
public void setT(T t);
}
// 前面已经提过,声明泛型才用T,使用泛型 就要指定参数的类型,所以在此指定具体的实际类型是String
public class ServiceImpl1 implements Service<String> {
private String val;
@Override
public String getT() {
System.out.println(val.getClass());
return this.val;
}
@Override
public void setT(String s) {
this.val = s;
}
public static void main(String[] args) {
//因为在定义ServiceImpl1时,已经指定了Service接口的T是String类型,所以此处不需要在指定T的类型了
ServiceImpl1 s1 = new ServiceImpl1();
s1.setT("str");
System.out.println(s1.getT());//输出:class java.lang.String str
}
}
在定义ServiceImpl1时,我们指定了Service的T是String,但是如果还需要一个ServiceImpl2来保证Service的T是Integer呢?甚至是Double呢?如果在定义ServiceImpl2、ServiceImpl3、ServiceImpl4就违背设计泛型的初衷了。所以泛型同样支持实现类本身也是泛型类。
// 声明泛型类ServiceImpl2用T,使用泛型ServiceImpl2 就要指定T具体的类型了
/*
但是注意,此处还使用到了泛型接口Service,使用泛型接口Service要指定具体的类型。
但是这个类型是由ServiceImpl2确定的,所以ServiceImpl2中的形参T作为Service接口的实参,传递给Service.
*/
/*
把service、ServiceImpl2当作方法就容易理解了。
比如 声明了一个Service方法,我们在使用Service方法是需要传递具体的实参。
然后ServiceImpl2方法内部调用了Service方法。那么ServiceImpl2方法的形参对于service来说,就是实参
*/
public class ServiceImpl2<T> implements Service<T > {
private T val;
@Override
public T getT() {
System.out.println(val.getClass());
return this.val;
}
@Override
public void setT(T t) {
this.val = t;
}
public static void main(String[] args) {
//String作为具体的类型传递给ServiceImpl2,ServiceImpl2内部会把String再传给Service
ServiceImpl2<String> s1 = new ServiceImpl2<>();
ServiceImpl2<Integer> s2 = new ServiceImpl2<>();
s1.setT("str");
s2.setT(111);
System.out.println(s1.getT());//class java.lang.String
System.out.println(s2.getT());//class java.lang.Integer
}
}
作用在方法上
public class TestMethod {
//同样使用 <T> 表明这是个泛型方法,并且指定类型的范围是T
public static <T> void show1(){
System.out.println("str");
}
public static <T> void show2(T t){
System.out.println(t.getClass());
System.out.println(t);
}
//泛型方法的返回值可以和泛型有关哦
public static <T> T show3(T t){
System.out.println(t.getClass());
System.out.println(t);
return t;
}
public static void main(String[] args) {
show1();
System.out.println("=====================================================");
show2("asd");
System.out.println("=====================================================");
String qwe = show3("qwe");
System.out.println(qwe);
}
}
泛型的上下界限定符 extends 、super
现在有一个新的需求,以Sout为例
public class Sout<T> {
private T val;
Sout(T val){
this.val = val;
}
public void sout(){
System.out.println(val.getClass());
System.out.println(val);
}
public static void main(String[] args) {
Sout<String> s1 = new Sout<>("str");
Sout<Integer> s2 = new Sout<>(111);
s1.sout();
s2.sout();
}
}
我们在使用Sout类时,可以直接指定类型,可以指定String、Integer等等。如果现在有一个新的需求,只允许Number的子类才能使用Sout,该怎么办呢?还记得上文中提到的<T>
有两个含义,1、表明这是个泛型类或者泛型接口。2、表明类型的范围是T。
那么如果需要限制类型的范围,直接在 “<>”中间限制类型范围即可。java提供了extends关键字,用于限制类型的范围。比如<T extends Number>
表明使用时 一定要指定具体的类型,并且这个类型必须是Number的子类。
上面的代码就可以改为:
public class Sout<T extends Number> {
private T val;
Sout(T val){
this.val = val;
}
public void sout(){
System.out.println(val.getClass());
System.out.println(val);
}
public static void main(String[] args) {
Sout<String> s1 = new Sout<>("str");//此时此行代码,会报错,报错信息:Type parameter 'java.lang.String' is not within its bound; should extend 'java.lang.Number'
Sout<Integer> s2 = new Sout<>(111);
s1.sout();
s2.sout();
}
}
同样,如果限制Sout的类型参数必须是Integer的父类,那么使用<T super Integer>
即可。
通配符 ?
现在同样有一个新的需求,我们需要一个专门处理泛型类Sout的方法。 要求泛型类Sout的参数类型必须是Number的子类,但是这个方法只针对Integer的子类进行处理。
我们可以使用泛型方法完成这个需求:
public class TestMethod2 {
// 定义泛型类Sout时,指定了参数类型必须是Number的子类
static class Sout<T extends Number> {
private T val;
Sout(T val){
this.val = val;
}
public T getVal() {
return val;
}
}
//show3方法只允许接受泛型类Sout,并且参数类型必须是Integer的子类
// 只需要在声明泛型的 “ <>标志 " 中限制类型的范围即可
public static <T extends Integer> void show(Sout<T > sout){
System.out.println(sout.getVal().getClass());
System.out.println(sout.getVal());
}
public static void main(String[] args) {
show(new Sout<Integer>(111));
}
}
那如果不允许使用泛型方法呢?java 提供了通配符(?)
完成这个功能。代码如下:
public static void show2(Sout<? extends Integer> sout){
System.out.println(sout.getVal().getClass());
System.out.println(sout.getVal());
}
public static void main(String[] args) {
//show(new Sout<Integer>(111));
show2(new Sout<Integer>(222));
}
通配符(?)
表示任意类型(不确定的java类型)。当我们在使用泛型时,无法确定泛型的具体的参数类型,可以使用通配符代替。划重点,1、使用泛型时。2、无法确定。这两点已经表达出了很重要的信息。
第一点,使用通配符的前提时,必须是一个泛型类。通配符的作用范围是变量
第二点,上文就说过,我们想要使用泛型,必须指定具体的类型,但是如果不确定的话,就可以使用通配符。
比如:
public class TestMethod3 {
// 此处我们无法确定Sout的具体参数类型,这个是动态传递过来的,此时可以用通配符代替
// 注意:? 虽然代表任意类型,但是这个“任意”的范围也要遵守 在定义泛型类Sout时,指定的范围,即extends Number
private Sout<?> sout;
public void setSout(Sout<?> sout) {
this.sout = sout;
}
public Sout<?> getSout() {
return sout;
}
public static void main(String[] args) {
TestMethod3 testMethod3 = new TestMethod3();
testMethod3.setSout(testMethod3.new Sout<Integer>(222) );
testMethod3.getSout().sout();
}
class Sout<T extends Number> {
private T val;
Sout(T val){
this.val = val;
}
public T getVal() {
return val;
}
public void sout(){
System.out.println(getVal().getClass());
System.out.println(getVal());
}
}
}
PECS原则
PESC原则有个大前提,就是这个规则是基于集合的。
首先来看下List的get以及add方法
public interface List<E> extends Collection<E> {
boolean add(E e)
E get(int index)
}
/*
当我们使用 List<? extends Number> list时,意味着可以向list添加任意类(只要是Number的子类)
那么,list.add(new Integer(1));此时向List中传入的Integer作为具体的类型。
但是接下来list.add(new Double(1.1)); 这个时候又传入了Double作为具体的类型。与Integer冲突。
所以,编译器不会允许向List<? extends XXX> 形式的集合添加任何元素。-
、
但是从List<? extends XXX> list形式的集合获取元素时允许的,
因为list中无论是Double还是Integer,都是Number类型。
*/
List<? extends Number> list = new ArrayList<>();
list.add(new Integer(1));//编译报错
list.add(new Double(1.1));//编译报错
Number number = list.get(0);//编译通过
结论: <? extends T>
只能从中获取数据,不能添加数据。像一个生成者一样,只从生产者 手中获取东西,这就是 Producer Extends (PE)
。
/*
当我们使用 List<? super Integer> list时,可以向list添加Integer类的父类。
此时我们传入一个Number类型作为具体的参数,这时,因为java中继承的传递性,凡是Integer的子类,就也一定是Number的子类,所以此时add(Integer的子类)是完全可以的。
但是因为,Integer的父类,父接口也有很多,所以编译器无法确定传入的到底是哪个父类,
所以get对象时,只能使用Object作为返回值。对于读操作而言,非常不合适。
*/
List<? super Integer> list = new ArrayList<>();
list.add(new Integer(1));
Object object = list.get(0);
结论:<? super Integer>
只能添加数据,不能获取数据(准确的说,只能使用Object接受对象,然而这样丧失了泛型的意义)。像消费者一样,需要你生产对象放进去。这就是 Consumer Super(CS)
。
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.add(2);
list1.add(3);
List<Integer> list2 = new ArrayList<>();
copy(list1,list2);
System.out.println("======================================");
System.out.println(Arrays.toString(list2.toArray()));
}
public static void copy(List<? extends Integer> list1,List<? super Integer> list2){
//list1.add(new Integer(1)); 编译报错
for (Integer integer : list1) {
list2.add(integer);
//Object object = list2.get(0);
}
}
T 和 ? 的区别
T
表示具体类型,作用在接口、类、方法上
?
表示任意类型,作用在变量上
有了泛型T
,才能使用 ?
。?
的初衷就是无法确定T
到底是什么类型,所以才有了?
。也就是说T
是?
的前提。只有使用<T>
声明了泛型类,后续在使用这个泛型类时,才有机会使用到?
List
和List<Object>
的区别
1、编译时,编译器不会对List
进行类型安全检查,会对List<Object>
这种带有参数的进行类型安全检查
2、可以把任何带参数的类型传递给原始类型List
,但却不能把List<Integer>
传递给接受 List<Object>
的方法,因为会产生编译错误
public static void show1(List list) { }
public static void show2(List<Object> list) { }
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
show1(list);//编译通过
show2(list);//编译不通过
}
List<?>
和 List<Object>
的区别
List<?>
表示可以存放任意类型。 List<Object>
表示传进去的具体类型是Object,不能把List<Integer>
传递给接受 List<Object>
的方法
public static void show1(List<?> list) { }
public static void show2(List<Object> list) { }
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
show1(list);//编译通过
show2(list);//编译不通过
}
类型擦除
泛型实际上是一个语法糖,在java文件编译为class文件的时候,就进行了类型擦除。
类型擦除:就是从泛型类中清除类型参数的相关信息,并且在必要的时候进行类型检查和类型转换。
先看个例子:
// 这是个java文件
public class Sout<T> {
private T val;
Sout(T val) {
this.val = val;
}
public void sout() {
System.out.println(this.val.getClass());
System.out.println(this.val);
}
public T getVal() {
return this.val;
}
public static void test(Sout<? extends Integer> asd) {
asd.sout();
}
}
// 这是编译后的class文件
public class Sout
{
private Object val;
Sout(Object val)
{
this.val = val;
}
public void sout()
{
System.out.println(val.getClass());
System.out.println(val);
}
public Object getVal()
{
return val;
}
public static void test(Sout asd)
{
asd.sout();
}
}
我们可以清楚的看到class文件中根本就没有T的影子。这是因为javac进行编译的时候,基于一个最左原则(可确定的 、最顶级的父类型)
将类型参数T
替换掉了。
所谓的最左原则换句话说:就是 如果T有上界(比如 T extends XXX),那么就使用XXX代替类型参数T
,否则使用Object来代替T,因为Object是所有类的父类嘛。
因此,会引发一个结果:泛型类实例的class都是同一个class,就是说 编译器只为泛型类生成一份字节码
。
既然class文件底层都没有泛型参数的信息了,那么相应的,我们使用泛型的java代码在编译的时候也会通过类型转换与检查来去除泛型,看例子:
// 这是java文件
public static void main(String[] args) {
Sout<String> s1 = new Sout<>("haha");
String val1 = s1.getVal();
s1.sout();
Sout<Integer> s2 = new Sout<>(2222);
Integer val2 = s2.getVal();
s2.sout();
}
//这是反编译后的class文件
public static void main(String args[])
{
Sout s1 = new Sout("haha");
String val1 = (String)s1.getVal();
s1.sout();
Sout s2 = new Sout(Integer.valueOf(2222));
Integer val2 = (Integer)s2.getVal();
s2.sout();
}