java基础:泛型(2022)

入职新公司也快转正了,是时候再看看总结之前的东西,毕竟最近各大公司裁员,寒冬真正降临了。希望下次到自己时候不那么焦虑,现在能做的就是提前准备好,随时保持自己可以直接去参加并通过面试。

泛型

直接问几个问题吧,看下是否能回答上。

1.泛型出现的背景、版本、目的?

2.泛型类怎么定义,泛型接口如何定义以及怎么使用?

3.泛型方法的定义格式,跟泛型类普通类是什么关系?

4.泛型能不能用基本数据类型,当a继承b,泛型a,和泛型b有没有关系?

5.泛型的继承怎么使用?

6.什么是泛型擦出?为什么要擦出?擦出了怎么知道是什么类型?知道Gson和Retrofit分别是怎么处理的吗?是否知道JRE和IDE对泛型的检查区别?

问题回答:

1.泛型出现的背景、版本、目的?

泛型是在JDK1.5版本引入,主要是在引入泛型之前,我们可能有一些类里面有很多的方法重载,比如一个计算工具类 ,里面只是输入的参数类型不同,我们需要写3个方法,其中很多重复代码:

         public void  add(int data,int data2){
            data =  data+data2;
        }
        public void  add(float data,float data2){
            data = data+data2;
        }
        public void  add(double data,double data2){
            data = data+data2;
        }

如果我们使用泛型:

class Utils {
            public void  add(T data ,T data2){
            }
        }

另外一个场景:
我们往一个List里面是可以添加任意类型的数据,本来是要添加int类型的来做计算,但没有泛型之前添加了一个string 类型的数据,就可能造成一些计算或者强制类型转换出错,这个出错发生在运行期间。所以泛型的出现目的是把运行时的问题提前到编译时发现。

结论:泛型是JDK1.5版本出现的,主要出现目的是在上面描述的一些场景把运行时的问题提前在编译期暴露出来,另外能让我们写更少的重复代码提高代码的封装。
2.泛型类怎么定义,泛型接口如何定义以及怎么使用?

泛型类也就是在类增加 < T >方式,T可以是任意的字母表示,泛型接口同样方式,一般实现一个泛型接口,要么也不指定具体的类型,也用T暂时替代,要么就明确指定类型如下:

        interface  IRequestListener {
            K success();
        }

        class ImplClass implements IRequestListener {

            @Override
            public T success() {
                return null;
            }
        }
        
        public void getData(){
            new IRequestListener(){
                @Override
                public String success() {
                    return null;
                }
            };
        }
3.泛型方法的定义格式,跟泛型类普通类是什么关系?

泛型方法的定义是对比普通方法 在修饰类型和返回值类型之间多了一个 < T >
如下:

class ImplClass implements IRequestListener {

            @Override
            public T success() {
                return null;
            }
          //public String getString(String s){} 
            public  K getData(K data){
                return data;
            } 
        }
4.泛型能不能用基本数据类型,当a继承b,泛型a,和泛型b有没有关系?

没有任何关系

 public class PP{
       }
       class A{}
       class  B extends A{}
        A a = new B();
        PP p = new PP(); //这个是会报错的,也就是泛型包装了的类不再是继承关系
5.泛型的继承怎么使用?

有两种使用方式:
? extends X 表示类型的上界,类型参数是X的子类
? super X 表示类型的下界,类型参数是X的超类
这个理解比较绕吧,个人理解,如果定义了一个Friut类, 使用 ? extends Friut类,里面提供get和set方法,哪个能用呢?很多高级工程师也说不清具体的:

//        testType< Fruit> tt = new testType(); //直接这么写会报错,需要下面的写法
        testType tt = new testType(); //但是这个写法后,只能拿,因为只知道这个是Fruit类型的,获取的肯定是Fruit,但是去设置,不确定是苹果还是别的水果
        Fruit data = tt.getData();
        tt.setData(new Apple()); //这个会报错


//        testType ttt = new testType(); //同样会报错
        testType< ? super Apple > ttt = new testType(); //同样拿实际的右边一定是苹果的父亲,但是是父亲还是祖先不确定,
        ttt.setData(new Apple());
        Object data1 = ttt.getData(); //这个获取的是

简单理解 ?extends T 这种是类型一定是T的子类,可以获取到T 但是不能设置数据,因为T有很多子孙;? extends X 表示类型的上界,类型参数是X的子类,那么可以肯定的说,get方法返回的一定是个X(不管是X或者X的子类)编译器是可以确定知道的。但是set方法只知道传入的是个X,至于具体是X的那个子类,不知道。
总结:主要用于安全地访问数据,可以访问X及其子类型,并且不能写入非null的数据。

?super T这种是T的超类,可以拿到具体的类型,但不能用set去限制。? super X 表示类型的下界,类型参数是X的超类(包括X本身),那么可以肯定的说,get方法返回的一定是个X的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以get方法返回Object。编译器是可以确定知道的。对于set方法来说,编译器不知道它需要的确切类型,但是X和X的子类可以安全的转型为X。
总结:主要用于安全地写入数据,可以写入X及其子类型。

6.什么是泛型擦出?为什么要擦出?擦出了怎么知道是什么类型?知道Gson和Retrofit分别是怎么处理的吗?是否知道JRE和IDE对泛型的检查区别?

我们在开发时如果写以下代码就会报错:

    public  String getData(ArrayList list){
        return "";
    }
    public  int getData(ArrayList list){
        return 123;
    }

因为存在泛型擦出问题,因为Java的范型只存在于源码里,编译的时候给你静态地检查一下范型类型是否正确,而到了运行时就不检查了。代码在JRE(Java运行环境)其实就是没有泛型类型的

泛型擦出的目的:
为了向下兼容。
Java 5才引入了范型,在此之前都是没有范型的。当时引入范型只是为了解决遍历集合的时候总要手动强制转型的问题,
比如我们打印下面的代码, 是一样的,如果为了证明,还可以反射往一个指定泛型为int类型的数据添加string能成功也说明了泛型擦出:

List list1 = new ArrayList<>();
List list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass());

retrofit/ Gson是怎么获得擦除后的类型的?
比如retrofit代码:

public interface GitHubService {
  @GET("users/{user}/repos")
  Call> listRepos(@Path("user") String user);
}

擦出后是这样子:

public interface GitHubService
{
     public abstract Call listRepos(String s);
}

那么Retrofit是如何拿到Call的类型信息?

static  ServiceMethod parseAnnotations(Retrofit retrofit, Method method) {
    ...
    Type returnType = method.getGenericReturnType();
    ...
  }
 
    public Type getGenericReturnType() {
       // 根据 Signature 信息 获取 泛型类型 
      if (getGenericSignature() != null) {
        return getGenericInfo().getReturnType();
      } else { 
        return getReturnType();
      }
    }

通过getGenericReturnType来获取类型信息的, 虽然被擦除了, 但是某些(声明侧的泛型,接下来解释) 泛型信息会被class文件 以Signature的形式 保留在Class文件的Constant pool中。

通过javap命令 可以看到在Constant pool中#5 Signature记录了泛型的类型

// Gson 常用的情况

    public  List parse(String jsonStr){
        List topNews =  new Gson().fromJson(jsonStr, new TypeToken>() {}.getType());
        return topNews;

声明侧泛型会被记录在Class文件的Constant pool中,使用侧泛型则不会
声明侧泛型主要指以下内容
1.泛型类,或泛型接口的声明 2.带有泛型参数的方法 3.带有泛型参数的成员变量

使用侧泛型
也就是方法的局部变量,方法调用时传入的变量。

Gson解析时传入的参数属于使用侧泛型,因此不能通过Signature解析
Class类提供了一个方法public Type getGenericSuperclass() ,可以获取到带泛型信息的父类Type。
也就是说java的class文件会保存继承的父类或者接口的泛型信息
所以Gson使用了一个巧妙的方法来获取泛型类型:
1.创建一个泛型抽象类TypeToken ,这个抽象类不存在抽象方法,因为匿名内部类必须继承自抽象类或者接口。所以才定义为抽象类。
2.创建一个 继承自TypeToken的匿名内部类, 并实例化泛型参数TypeToken
3.通过class类的public Type getGenericSuperclass()方法,获取带泛型信息的父类Type,也就是TypeToken

总结:Gson利用子类会保存父类class的泛型参数信息的特点。 通过匿名内部类实现了泛型参数的传递。

kotlin中的泛型

out TextView === ? extends TextView
out 意思就是我只能输出,不能输入,只能读我,不能写我

in TextView === ? super TextView
in 意思只能写不能读,因为不知道我具体的类型,


截屏2022-05-21 下午1.17.13.png

你可能感兴趣的:(java基础:泛型(2022))