一、学习目标
1.泛型的作用和定义
2.泛型的基本使用
3.泛型中的通配符
4.泛型擦除
5.泛型中的约束和局限
二、泛型的作用和定义
1.引言
Java属于强类型语言,所定义的每一个对象都具有明确的类型(基本类型+其他自定义类型),关于语言类型,专业的一些解释。
由此在定义一些容器对象时需要明确容器的元素类型,或者使用Object类型。
方式一:枚举容器能够接纳和记录每一个类型
package com.java.basis.generic;
/**
* 定义每一个类型独立的方法
*/
public class NoTypeGeneric {
private String stringconfig;
private int intconfig;
private float floatconfig;
private User userconfig;
public void addConfig(String config){
this.stringconfig = config;
}
public void addConfig(int config){
this.intconfig = config;
}
public void addConfig(float config){
this.floatconfig = config;
}
public void addConfig(User config){
this.userconfig = config;
}
public String getStringConfig(){
return stringconfig;
}
public int getIntConfig(){
return intconfig;
}
public float getfloatConfig(){
return floatconfig;
}
public User getUserConfig(){
return userconfig;
}
}
使用
NoTypeGeneric noTypeGeneric = new NoTypeGeneric();
User user = new User("张三", 18);
noTypeGeneric.addConfig(user);
User valus = noTypeGeneric.getUserConfig();
通过枚举的方式来实现容器能够装入和取出不同的元素时,容器需要提供足够多的类型对应的方法。
方式二:通过父类Object来实现能够承载不同类型的容器
package com.java.basis.generic;
/**
* 定义所有进容器的类型为Object
*/
public class NoGeneric {
private Object config;
public void addConfig(Object config){
this.config = config;
}
public Object getConfig(){
return config;
}
}
使用:
NoGeneric noGeneric = new NoGeneric();
noGeneric.addConfig(1);
//强制类型转换
int value = (int) noGeneric.getConfig();
该方式也是JDK1.5之前Java容器的实现方式,Java容器都是通过将类型向上转型为Object类型来实现的,因此在从容器中取出来的时候需要手动的强制转换。此方式带来的问题是代码在编译期间是无法知道类型转换是否合法,不合法则会导致ClassCastException。
2.泛型的作用和定义
- 作用:
1.使用泛型能写出更加灵活通用的代码
一个容器同一个方法可以根据输入的不同类型来生产对应类型的结果,实现了多类型复用
2.泛型将代码安全性检查提前到编译期
能让编译器在编译的时候借助传入的类型参数检查对容器的插入,获取操作是否合法,从而将运行时ClassCastException转移到编译时 - 定义:
泛型就是将所操作的数据类型作为参数的一种语法。
三、泛型基本使用
泛型分为:
1 : 泛型接口 interface Observer<T>{}
2 : 泛型类 class ImplObserver<T> {}
3 : 泛型方法 private <T> void<T> call(T t)
3.1泛型类
泛型类在实体化的需要指定泛型参数的具体类型,泛型类中的泛型方法是一个全新的泛型参数类型
/**
* 自定义泛型,泛型类定义的泛型参数只作用于普通的方法
* @param <T>
*/
public class GenericClass<T> {
private T config;
public void addConfig(T config){
this.config = config;
}
public T getConfig(){
return config;
}
/**
* 泛型类中定义的泛型方法,是一个全新的类型
* @param a
* @param <T>
*/
public <T> void show(T a){
System.out.println(a);
}
public static void main(String[] args) {
User user2 = new User("张三", 18);
GenericClass<User> userGeneric = new GenericClass<>();
userGeneric.addConfig(user2);
System.out.println(userGeneric.getConfig().getName());
userGeneric.show("s");
}
}
3.2泛型接口
public interface IGeneric<T> {
public T next();
}
泛型接口的实现方式
方式一:实现时未指定泛型参数的具体类型,那么该类将成为泛型类,实例化时需要指定类型
/**
* 类实现泛型接口如果不指定泛型的具体类型时,该类将成为泛型类
* @param <T>
*/
public class ImpGeneric<T> implements IGeneric<T>{
@Override
public T next() {
return null;
}
}
方式二:实现时指定泛型参数的具体类型,泛型接口的泛型参数在实现的时候将会具体化,实例化时不需要指定类型
/**
* 类实现泛型接口如果指定了泛型的具体类型时,泛型接口的泛型参数在实现的时候将会具体化
*
*/
public class ImpGeneric2 implements IGeneric<String>{
@Override
public String next() {
return null;
}
public static void main(String[] args) {
ImpGeneric2 impGeneric = new ImpGeneric2();
impGeneric.next();
}
}
3.3泛型方法
泛型方法,是在调用方法的时候指明泛型的具体类型 ,泛型方法可以在任何地方和任何场景中使用,包括普通类和泛型类。
public class GenericMethod {
public <T> T getConfig(T...a){
System.out.println(a.getClass().getName().toString());
System.out.println(a.length);
return a[a.length/2];
}
public static void main(String[] args) {
GenericMethod genericMethod = new GenericMethod();
String t = genericMethod.getConfig("adb", "ahb", "shshsh", "uuuu");
System.out.println(t);
}
}
四、泛型中的通配符
4.1常用的 T,E,K,V,?
本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西
-
T(type) 表示具体的一个java类型,我们可以换成 A-Z 之间的任何一个 字母都可以
-
K V (key value) 分别代表java键值中的Key Value
-
E (element) 代表Element
-
?表示不确定的 java 类型 ,无界通配符
4.2 ?和 T 的区别
区别一:通过 T 来 确保 泛型参数的一致性
public class TAndQuestionmark {
public static <T> void show1(List<T> list){
for (Object object : list) {
System.out.println(object.toString());
}
}
public static void show2(List<?> list) {
for (Object object : list) {
System.out.println(object.toString());
}
}
public static void main(String[] args) {
List<Student> list1 = new ArrayList<>();
list1.add(new Student("zhangsan",18,0));
list1.add(new Student("lisi",28,0));
list1.add(new Student("wangwu",24,1));
// 通过 T 来 确保 泛型参数的一致性
//这里如果add(new Teacher(...));就会报错,因为我们已经给List指定了数据类型为Student
show1(list1);
System.out.println("************分割线**************");
//这里我们并没有给List指定具体的数据类型,可以存放多种类型数据
List list2 = new ArrayList<>();
list2.add(new Student("zhaoliu",22,1));
list2.add(new User("sunba",30));
show2(list2);
}
}
运行结果:
Student{name='zhangsan', age=18, id=0}
Student{name='lisi', age=28, id=0}
Student{name='wangwu', age=24, id=1}
************分割线**************
Student{name='zhaoliu', age=22, id=1}
User{name='sunba', age=30}
区别二:类型参数T可以多重限定,而通配符 ?不行,通配符 ?不能用于定义类和泛型方法
/**
* 此时T的类型限定为必须InterFaceA 和 InterFaceB 的实现类
* @param config
* @param <T>
*/
public static <T extends InterFaceA & InterFaceB> void setGenericConfig(T config){
}
interface InterFaceA{}
interface InterFaceB{}
class MultiInterface implements InterFaceA, InterFaceB{
}
class MultiInterfaceA implements InterFaceA{
}
public static <T extends InterFaceA & InterFaceB> void setGenericConfig(T config){
}
// 不允许
// public static <? extends InterFaceA & InterFaceB> void setGenericConfig(? config){
//
// }
MultiInterfaceA multiInterfaceA = new MultiInterfaceA();
//报错不允许
setMultiGenericConfig(multiInterfaceA);
MultiInterface multiInterfaceAB = new MultiInterface();
setMultiGenericConfig(multiInterfaceAB);
使用 & 符号设定多重边界(Multi Bounds),指定泛型类型 T 必须是 MultiLimitInterfaceA 和 MultiLimitInterfaceB 的共有子类型,此时变量 t 就具有了所有限定的方法和属性。对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定。
区别三:通配符 ? 可以使用超类限定,而类型参数T不行
类型参数 T 只具有 一种 类型限定方式:
T extends A
但是通配符 ? 可以进行 两种限定:
? extends A
? super A
总结:T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。
4.3上界通配符 < ? extends E> 和< T extends E>
< ? extends E>和< T extends E>作用一样,但是指定T可以确保 泛型参数的一致性,具体看下面?和 T 的区别
用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:
-
如果传入的类型不是 E 或者 E 的子类,编译不成功
-
泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用
使用: 类型参数列表中如果有多个类型参数上限,用逗号分开
private <K extends A, E extends B> E test(K arg1, E arg2){
E result = arg2;
arg2.compareTo(arg1);
//.....
return result;
}
同时extends左右都允许有多个,如 T,V *extends* Comparable & Serializable
注意限定类型中,只允许有一个类,而且如果有类,这个类必须是限定列表的第一个。
get和set:
set方法是不允许被调用的,会出现编译错误,? extends X 表示类型的上界,类型参数是X的子类,那么可以肯定的说,get方法返回的一定是个X(不管是X或者X的子类)编译器是可以确定知道的。但是set方法只知道传入的是个X,至于具体是X的那个子类,不知道。
总结:主要用于安全地访问数据,可以访问X及其子类型,并且不能写入非null的数据。
4.4下界通配符 < ? super E>
用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object
在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。
实际的应用场景,合并两个list时,假如源数据src的类型范围大于合成的目标数据范围,则无法实现合并的需求:
dst 类型 “大于等于” src 的类型,这里的“大于等于”是指 dst 表示的范围比 src 要大,因此装得下 dst 的容器也就能装 src 。
private <T> void test(List<? super T> dst, List<T> src){
for (T t : src) {
dst.add(t);
}
}
public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = new ArrayList<>();
new Test3().test(animals,dogs);
}
// Dog 是 Animal 的子类
class Dog extends Animal {
}
get和set:
? super X 表示类型的下界,类型参数是X的超类(包括X本身),那么可以肯定的说,get方法返回的一定是个X的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以get方法返回Object。编译器是可以确定知道的。对于set方法来说,编译器不知道它需要的确切类型,但是X和X的子类可以安全的转型为X。
总结:主要用于安全地写入数据,可以写入X及其子类型。
4.5Class<T>和Class<?>
具体
public Class<?> clazz;
// 不可以,因为 T 需要指定类型,除非定义类时指定A的类型
public Class<A> clazzT;
public static void main(String [] args) throws InstantiationException, IllegalAccessException {
A a = createInstance(A.class);
B b = createInstance(B.class);
}
class A{}
class B{}
public static <A> A createInstance(Class<A> aClass) throws IllegalAccessException, InstantiationException {
return aClass.newInstance();
}
Class<T>在实例化的时候,T 要替换成具体类。
Class<?>它是个通配泛型,? 可以代表任何类型,所以主要用于声明时的限制情况。
那如果也想 public Class clazzT;
这样的话,就必须让当前的类也指定 T ,
public class Test3<T> {
public Class<?> clazz;
// 不会报错
public Class<T> clazzT;
}
4.6泛型嵌套
定义两个泛型类,Myclass类的泛型就是student类,而student类的泛型是String类
class Student<T>{
private T score;
public T getScore(){
return score;
}
public void setScore(T score){
this.score=score;
}
}
class MyClass<T>{
private T cls;
public T getCls(){
return cls;
}
public void setCls(T cls){
this.cls=cls;
}
}
public static void main(String[] args) {
Student<String> stu=new Student<String>();
stu.setScore("great");
//泛型嵌套
MyClass<Student<String>> cls=new MyClass<Student<String>>();
cls.setCls(stu);
Student<String> stu2=new Student<String>();
stu2=cls.getCls();
System.out.println(stu2.getScore());//great
}
如上就实现了泛型的嵌套,在HsahMap中对键值对进行便利的时候,也利用了泛型的嵌套
public static void main(String[] args) {
Map<String,String> map=new HashMap<String,String>();
map.put("a", "java300");
map.put("b", "马士兵javase");
Set<Entry<String,String>> entrySet=map.entrySet();
for(Entry<String,String> entry:entrySet){
String key=entry.getKey();
String value=entry.getValue();
}
}
五、泛型擦除
Java语言的泛型采用的是擦除法实现的伪泛型,泛型信息(类型变量、参数化类型)编译之后通通被除掉了。
泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换成它们非泛型上界。
为了验证擦除,我们看下面分析:
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
GenericType<Food> a = new GenericType<>();
GenericType<Food> b = new GenericType<>();
//报错
// if(a instanceof GenericType<Fruit>){
//
// }
System.out.println(a.getClass() == b.getClass());
执行结果为
true
true
ArrayList<String>和ArrayList<Integer>在运行时事实上是相同的类型。这两种类型都被擦除成它们的“原生”类型,即ArrayList。
所以不能获取或者用instanceof等关键字来判断泛型的具体类型,只能得到原始类型。
在JDK1.5后Signature属性被增加到了Class文件规范中,它是一个可选的定长属性,可以出现在类、字段表和方法表结构的属性表中。在JDK1.5中大幅度增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Singature属性会为它记录泛型签名信息。Signature属性就是为了弥补擦除法的缺陷而增设的,Java可以通过反射获得泛型类型,最终的数据来源也就是这个属性。
可以看class编码如下:
image-20201112015210025从Signature属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。
六、泛型中的约束和局限性
5.1不能用基本类型实例化类型参数
泛型要求包容的是对象类型,而基本数据类型在Java中不属于对象。但是基本数据类型有其封装类,且为对象类型。
5.2因为泛型擦除,运行时类型查询只适用于原始类型
Restrict<Double> restrict = new Restrict<>();
// if(restrict instanceof Restrict<Double>)
// if(restrict instanceof Restrict<T>)
Restrict<String> restrictString= new Restrict<>();
System.out.println(restrict.getClass()==restrictString.getClass());
System.out.println(restrict.getClass().getName());
System.out.println(restrictString.getClass().getName());
5.2泛型类的静态上下文中类型变量失效
不能在静态域或方法中引用类型变量。因为泛型是要在对象创建的时候才知道是什么类型的,而对象创建的代码执行先后顺序是static的部分,然后才是构造函数等等。所以在对象初始化之前static的部分已经执行了,如果你在静态部分引用的泛型,那么毫无疑问虚拟机根本不知道是什么东西,因为这个时候类还没有初始化。
5.3不能创建参数化类型的数组
Restrict<Double>[] restrictArray;
//Restrict<Double>[] restricts = new Restrict<Double>[10];
//ArrayList<String>[] list1 = new ArrayList<String>[10];
//ArrayList<String>[] list2 = new ArrayList[10];
5.4不能实例化类型变量
//不能实例化类型变量
// public Restrict() {
// this.data = new T();
// }
5.5不能捕获泛型类的实例
public class ExceptionRestrict {
/*泛型类不能extends Exception/Throwable*/
//private class Problem<T> extends Exception;
/*不能捕获泛型类对象*/
// public <T extends Throwable> void doWork(T x){
// try{
//
// }catch(T x){
// //do sth;
// }
// }
public <T extends Throwable> void doWorkSuccess(T x) throws T{
try{
}catch(Throwable e){
throw x;
}
}
}
七、总结
1.泛型就是将所操作的数据类型作为参数的一种语法,作用:A:使用泛型能写出更加灵活通用的代码;B:泛型将代码安全性检查提前到编译期;C:泛型会在编译期被擦出,导致类型无法获取泛型具体类型,或者方法重载冲突,但是JDK1.5之后加入Signature规范记录了泛型具体类型,所以并没有真正的擦除泛型的具体类型,当然我们也可以使用反射获取泛型的具体类型;
2.泛型接纳的是对象类型,不能用基本类型实例化类型参数;
3.泛型的类型查询以及继承关系,都归属于原始类型;
4.可以声明参数化类型的数组,但是不能创建;
5.?通配符可以实现下界以及多多重限定,而多重限定中,只允许有一个类,而且如果有类,这个类必须是限定列表的第一个;
6.extends限定上线,主要用于数据访问,能够get到明确的类型,而super主要用于安全地写入数据,可以写入X及其子类型。
网友评论