欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 家装 > Java泛型

Java泛型

2025/2/2 22:52:18 来源:https://blog.csdn.net/YLXCA/article/details/143896758  浏览:    关键词:Java泛型

目录

一、泛型是什么

二、为什么要引入泛型

三、泛型的基本使用

3.1、泛型类

3.2、泛型接口

3.3、泛型方法

3.4、泛型的上下限

3.4.1 上限

3.4.2 下限

四、深入理解泛型

4.1、泛型擦除

4.1.1 泛型的类型擦除原则

4.1.2 如何进行擦除

4.2、泛型的编译期检查

4.3、泛型的多态(桥方法)

4.4、如何理解基本类型不能作为泛型类型?

4.5、如何获取泛型的参数类型?



一、泛型是什么

泛型 ,顾名思义就是 广泛的数据类型,也就是说什么数据类型都可以

一般来说,我们见到的泛型就是这个样子,用 T 表示

如下所示,在类名后方申明泛型 T,接着就可以在成员变量、方法中使用泛型了。

public class User<T> {private T name;
}

二、为什么要引入泛型

1、适用于多种数据类型执行相同的代码(代码复用)

2、泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型

引入泛型,它将提供类型的约束,提供编译前的检查:

List<String> list = new ArrayList<String>();// list中只能放String, 不能放其它类型的元素

三、泛型的基本使用

3.1、泛型类

class Point<T>{         // 此处可以随便写标识符号,T是type的简称  private T var ;     // var的类型由T指定,即:由外部指定  public T getVar(){  // 返回值的类型由外部决定  return var ;  }  public void setVar(T var){  // 设置的类型也由外部决定  this.var = var ;  }  
}  
public class GenericsDemo06{  public static void main(String args[]){  Point<String> p = new Point<String>() ;     // 里面的var类型为String类型  p.setVar("it") ;                            // 设置字符串  System.out.println(p.getVar().length()) ;   // 取得字符串的长度  }  
}

3.2、泛型接口

interface Info<T>{        // 在接口上定义泛型  public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型  
}  
class InfoImpl<T> implements Info<T>{   // 定义泛型接口的子类  private T var ;             // 定义属性  public InfoImpl(T var){     // 通过构造方法设置属性内容  this.setVar(var) ;    }  public void setVar(T var){  this.var = var ;  }  public T getVar(){  return this.var ;  }  
} 
public class GenericsDemo24{  public static void main(String arsg[]){  Info<String> i = null;        // 声明接口对象  i = new InfoImpl<String>("汤姆") ;  // 通过子类实例化对象  System.out.println("内容:" + i.getVar()) ;  }  
}  

3.3、泛型方法

泛型方法,是在调用方法的时候指明泛型的具体类型。重点看下泛型的方法

  • 定义泛型方法语法格式
  • 调用泛型方法语法格式

说明一下,定义泛型方法时,必须在返回值前边加一个<T>,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。

Class<T>的作用就是指明泛型的具体类型,而Class<T>类型的变量c,可以用来创建泛型类的对象。

3.4、泛型的上下限

在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

3.4.1 上限

class Info<T extends Number>{    // 此处泛型只能是数字类型private T var ;        // 定义泛型变量public void setVar(T var){this.var = var ;}public T getVar(){return this.var ;}public String toString(){    // 直接打印return this.var.toString() ;}
}
public class demo1{public static void main(String args[]){Info<Integer> i1 = new Info<Integer>() ;        // 声明Integer的泛型对象}
}

3.4.2 下限

class Info<T>{private T var ;        // 定义泛型变量public void setVar(T var){this.var = var ;}public T getVar(){return this.var ;}public String toString(){    // 直接打印return this.var.toString() ;}
}
public class GenericsDemo21{public static void main(String args[]){Info<String> i1 = new Info<String>() ;        // 声明String的泛型对象Info<Object> i2 = new Info<Object>() ;        // 声明Object的泛型对象i1.setVar("hello") ;i2.setVar(new Object()) ;fun(i1) ;fun(i2) ;}public static void fun(Info<? super String> temp){    // 只能接收String或Object类型的泛型,String类的父类只有Object类System.out.print(temp + ", ") ;}
}

小结

<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。

四、深入理解泛型

4.1、泛型擦除

Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。

4.1.1 泛型的类型擦除原则

  • 消除类型参数声明,即删除<>及其包围的部分。
  • 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
  • 为了保证类型安全,必要时插入强制类型转换代码。
  • 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。

4.1.2 如何进行擦除

  • 擦除类定义中的类型参数 - 无限制类型擦除

当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如<T><?>的类型参数都被替换为Object。

  • 擦除类定义中的类型参数 - 有限制类型擦除

当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number><? extends Number>的类型参数被替换为Number<? super Number>被替换为Object。

4.2、泛型的编译期检查

既然说类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?

Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。

4.3、泛型的多态(桥方法)

类型擦除会造成多态的冲突,而JVM解决方法就是桥接方法。

现在有这样一个泛型类:

class Pair<T> {  private T value;  public T getValue() {  return value;  }  public void setValue(T value) {  this.value = value;  }  
}

然后我们想要一个子类继承它。

class DateInter extends Pair<Date> {  @Override  public void setValue(Date value) {  super.setValue(value);  }  @Override  public Date getValue() {  return super.getValue();  }  
}

在这个子类中,我们设定父类的泛型类型为Pair<Date>,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型。

public Date getValue() {  return value;  
}  public void setValue(Date value) {  this.value = value;  
}

所以,我们在子类中重写这两个方法一点问题也没有,实际上,从他们的@Override标签中也可以看到,一点问题也没有,实际上是这样的吗?

分析:实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:

class Pair {  private Object value;  public Object getValue() {  return value;  }  public void setValue(Object  value) {  this.value = value;  }  
} 

再看子类的两个重写的方法的类型:

@Override  
public void setValue(Date value) {  super.setValue(value);  
}  
@Override  
public Date getValue() {  return super.getValue();  
}

先来分析setValue方法,父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果实在普通的继承关系中,根本就不会是重写,而是重载。 我们在一个main方法测试一下:

public static void main(String[] args) throws ClassNotFoundException {  DateInter dateInter = new DateInter();  dateInter.setValue(new Date());                  dateInter.setValue(new Object()); //编译错误  
}

如果是重载,那么子类中两个setValue方法,一个是参数Object类型,一个是Date类型,可是我们发现,根本就没有这样的一个子类继承自父类的Object类型参数的方法。所以说,却是是重写了,而不是重载了。

为什么会这样呢

原因是这样的,我们传入父类的泛型类型是Date,Pair<Date>,我们的本意是将泛型类变为如下:

class Pair {  private Date value;  public Date getValue() {  return value;  }  public void setValue(Date value) {  this.value = value;  }  
}

然后再子类中重写参数类型为Date的那两个方法,实现继承中的多态。

可是由于种种原因,虚拟机并不能将泛型类型变为Date,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。JVM知道你的本意吗?知道!!!可是它能直接实现吗,不能!!!如果真的不能的话,那我们怎么去重写我们想要的Date类型参数的方法啊。

于是JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。

首先,我们用javap -c className的方式反编译下DateInter子类的字节码,结果如下:

class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {  com.tao.test.DateInter();  Code:  0: aload_0  1: invokespecial #8                  // Method com/tao/test/Pair."<init>":()V  4: return  public void setValue(java.util.Date);  //我们重写的setValue方法  Code:  0: aload_0  1: aload_1  2: invokespecial #16                 // Method com/tao/test/Pair.setValue:(Ljava/lang/Object;)V  5: return  public java.util.Date getValue();    //我们重写的getValue方法  Code:  0: aload_0  1: invokespecial #23                 // Method com/tao/test/Pair.getValue:()Ljava/lang/Object;  4: checkcast     #26                 // class java/util/Date  7: areturn  public java.lang.Object getValue();     //编译时由编译器生成的桥方法  Code:  0: aload_0  1: invokevirtual #28                 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法;  4: areturn  public void setValue(java.lang.Object);   //编译时由编译器生成的桥方法  Code:  0: aload_0  1: aload_1  2: checkcast     #26                 // class java/util/Date  5: invokevirtual #30                 // Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法)V  8: return  
}

从编译的结果来看,我们本意重写setValue和getValue方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。

不过,要提到一点,这里面的setValue和getValue这两个桥方法的意义又有不同。

setValue方法是为了解决类型擦除与多态之间的冲突。

而getValue却有普遍的意义,怎么说呢,如果这是一个普通的继承关系:

那么父类的getValue方法如下:

public Object getValue() {  return super.getValue();  
}

而子类重写的方法是:

public Date getValue() {  return super.getValue();  
}

其实这在普通的类继承中也是普遍存在的重写,这就是协变。

并且,还有一点也许会有疑问,子类中的桥方法Object getValue()Date getValue()是同时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。

4.4、如何理解基本类型不能作为泛型类型?

比如,我们没有ArrayList<int>,只有ArrayList<Integer>, 为何?

因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储int值,只能引用Integer的值。

另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作。

4.5、如何获取泛型的参数类型?

既然类型被擦除了,那么如何获取泛型的参数类型呢?可以通过反射(java.lang.reflect.Type)获取泛型

java.lang.reflect.Type是Java中所有类型的公共高级接口, 代表了Java中的所有类型. Type体系中类型的包括:数组类型(GenericArrayType)、参数化类型(ParameterizedType)、类型变量(TypeVariable)、通配符类型(WildcardType)、原始类型(Class)、基本类型(Class), 以上这些类型都实现Type接口。

public class GenericType<T> {private T data;public T getData() {return data;}public void setData(T data) {this.data = data;}public static void main(String[] args) {GenericType<String> genericType = new GenericType<String>() {};Type superclass = genericType.getClass().getGenericSuperclass();//getActualTypeArguments 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; System.out.println(type);//class java.lang.String}
}

其中 ParameterizedType:

public interface ParameterizedType extends Type {// 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]Type[] getActualTypeArguments();//返回当前class或interface声明的类型, 如List<?>返回ListType getRawType();//返回所属类型. 如,当前类型为O<T>.I<S>, 则返回O<T>. 顶级类型将返回null Type getOwnerType();
}

参考文章:

  • https://blog.csdn.net/sunxianghuang/article/details/51982979
  • https://blog.csdn.net/LonelyRoamer/article/details/7868820
  • https://docs.oracle.com/javase/tutorial/extra/generics/index.html
  • https://blog.csdn.net/s10461/article/details/53941091
  • https://www.cnblogs.com/iyangyuan/archive/2013/04/09/3011274.html
  • https://www.cnblogs.com/rudy-laura/articles/3391013.html
  • https://www.jianshu.com/p/986f732ed2f1
  • https://blog.csdn.net/u011240877/article/details/53545041

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com