Android泛型详解

参考文献:https://pingfangx.github.io/java-tutorials/java/generics/types.html 

1,什么是泛型?

Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了 编译时类型安全检测机制, 该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数类型,也就是说所操作的数据类型被指定为一个参数。 泛型不存在于JVM虚拟机。

通俗点讲,就是将类型参数化,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这样提高了代码的类型安全性,使你在编译时可以检测到更多错误。

2,为什么要使用泛型?

泛型在定义类,接口和方法时使类型(类和接口)成为参数。与方法声明中使用的形式参数非常相似,类型参数为你提供了一种使用不同输入重复使用相同代码的方法。区别在于形式参数的输入是值,而类型参数的输入是类型。
 
下面来看一个官方的例子
 
使用非泛型的代码如下:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);//需要强制转换

list.add(1);
s =(String) list.get(1); //在编译时期没有任何错误提示 在运行时期会报错

使用泛型的代码如下:

List list = new ArrayList();
list.add("hello");
String s = list.get(0); //不需要进行强制转换

list.add(1);
s =list.get(0); //会在编译期就报错 Required type:String  Provided:int

通过上面官方例子,我们不难发现,与非泛型代码相比,使用泛型的代码具有以下优点:

1,在编译时进行更强的类型检查。Java编译器将强类型检查应用于通用代码,如果代码违反类型安全,则会发出错误。修复编译时错误比修复运行时错误容易,后者可能很难找到。

2,消除类型转换。当使用泛型重写时,代码不需要强制转换。

3,使程序员能够实现通用算法。通过使用泛型,程序员可以实现对不同类型的集合进行工作,可以自定义并且类型安全且易于阅读的泛型算法。

 

3,泛型类的创建

泛型类是通过类型进行参数化的通用类。

泛型类的定义格式如下:

class 类名 {
    
    private 泛型标识  变量名;

}
//例如:
class Student {
    
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }

}

上面T可以随便定义,只要是在<>中,你甚至可以定义为dnadnandn都行,注意:尽量不要定义为关键字,以免引起冲突。

注意:类型参数与类型变量的区别:

Student中的T为类型参数

Student中的String为类型变量

常用的类型参数名称是:

T - Type类型 最常用
E - 元素或Exception异常
K - Key
V - Value
N - 数字
S,U,V 等:第二,第三,第四个类型

下面看一个简单的例子

泛型类:

public class MyList {

    private List list;

    public void add(T t){
        list.add(t);
    }

    public T get(int i){
       return list.get(i);
    }
    public int size(){
        return list.size();
    }

}

不使用泛型的类:

public class MyList1 {

    private List list;

    public void add(Object t){
        list.add(t);
    }

    public Object get(int i){
        return list.get(i);
    }

    public int size(){
        return list.size();
    }
} 
  

在使用时:

 //泛型类的使用
 MyList myList =new MyList<>();
 myList.add("aaa");
 //myList.add(1);  在编译时就报错
 for (int i = 0; i < myList.size(); i++) {
   String s1 = myList.get(i);
   System.out.println("s1="+s1);
 }

 //非泛型类的使用
 MyList1 myList1 =new MyList1();
 myList1.add(11);//不会报错
 myList1.add("111");
 for (int i = 0; i < myList1.size(); i++) {
     String s1 = (String) myList1.get(i);//1,需要强转 2,在运行时会报错
     System.out.println("s1="+s1);
 }

泛型类还可以传入多个泛型参数,下面看一个 两个参数的泛型类:

public class People {

    private K name;

    private V achievement;

    public People(K name, V achievement) {
        this.name = name;
        this.achievement = achievement;
    }

    public void setName(K name) {
        this.name = name;
    }

    public void setAchievement(V achievement) {
        this.achievement = achievement;
    }

    public K getName() {
        return name;
    }

    public V getAchievement() {
        return achievement;
    }
}
People p1 =new People<>("张三",98);
People p2 =new People<>("赵武",99);
//   People p3 =new People<>("赵武","100");//编译阶段报错

需要注意的是泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数,例如:

public class Test {

    private static T name;//编译时报错'com.yuanzhen.Test.this' cannot be referenced from a     static context

    public static T getName(){//编译时报错'com.yuanzhen.Test.this' cannot be referenced from a static context
        return name;
    }
}

原因是泛型在对象创建时才知道是什么类型,但是静态方法属于类,调用getName方法实际调用的Test类的方法,而类在编译阶段就存在了,所以虚拟机根本不知道方法中引用的泛型是什么类型
初始化时:对象创建的代码执行先后顺序是static的部分,然后才是构造函数等等,所以在对象初始化之前static的部分已经执行了,如果你在静态部分引用的泛型,那么毫无疑问虚拟机根本不知道是什么东西

4,泛型接口的创建

泛型接口的定义和泛型类的定义一样,区别就在于一个是接口需要实现各个接口方法,一个是类,需要实现对应的抽象方法。

泛型接口的定义格式如下:

public interface 接口名<类型参数> {
    ...
}
//例如
public interface IPeople {
    T getName();
    void setName(T t);
}

泛型的具体使用方式:

①直接在实现类中指定泛型的具体类型

public class Student implements IPeople{
    @Override
    public String getName() {
        return null;
    }

    @Override
    public void setName(String string) {

    }
}

②在实现类中继续使用泛型,在实例化实现类对象的时候指定泛型的具体类型

public class Teacher implements IPeople{
    @Override
    public T getName() {
        return null;
    }

    @Override
    public void setName(T t) {

    }
}
Teacher teacher =new Teacher<>();
teacher.setName("张三");

③在接口继承接口中指定泛型的具体类型。

public interface ITeacher extends IPeople{

}

5,泛型方法的创建

泛型方法是指引入自己的类型参数的方法。这类似于声明一个泛型方法,但类型参数的范围仅限于声明它的方法。允许使用静态和非静态的泛型方法,也允许使用泛型类构造函数。
泛型方法的语法包括类型参数列表,在尖括号内,该列表出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。
 
泛型方法的格式如下:
 
public <类型参数1,类型参数2...> 返回类型 方法名(类型参数1 变量名1,类型参数2 变量名2 ...) {
    ...
}
//例如:
public class Doctor {
    
    /**
    * 泛型方法
    */
    public  void doWork(T t){

    }
}

注意:只有在方法声明中声明了<>的方法才是泛型方法

下面看个例子:

public class Doctor {
    
    private T name;
    
    /*
    * 不是泛型方法 只是普通方法
    * */
    public Doctor(T name) {
        this.name = name;
    }
    /*
     * 不是泛型方法 只是普通方法
     * */
    public void setName(T name) {
        this.name = name;
    }
    /*
     * 不是泛型方法 只是普通方法
     * */
    public T getName() {
        return name;
    }
    /*
     * 泛型方法 因为它定义了自己的
     * */
    public  void doWork(T t){

    }
}

调用泛型方法:

Doctor doctor =new Doctor<>("张三");
doctor.doWork("看病");//调用泛型方法的完整语法
doctor.doWork("看病");//因为类型推断,所以可以省略 

类型推断:上文中可以看出,因为类型推断,可以省略<>,那么什么是类型推断呢?

类型推断(Type Inference)是指 Java 编译器能查看每个方法的调用和相应声明,以确定调用合适的类型参数(Type Argument)或参数。推断算法决定参数的类型,如果可用,则指定被赋值的类型或返回的类型。最后,推断算法试图在一起工作的所有参数中找到最具体的类型。

引入类型推断的泛型方法,能够让你像调用普通方法一样调用泛型方法,而不需要在尖括号中指定类型。

5,限定类型参数

有时你可能想限制可以在参数化类型中用作类型参数的类型。例如,对数字进行操作的方法可能只希望接受 Number 或其子类的实例。这就是限定类型参数的用途。
 
要声明一个限定的类型参数,请列出类型参数的名称,然后列出 extends 关键字,然后列出其上限(在本示例中为 Number )。请注意,在这种情况下, extends 通常用于表示“扩展”(如在类中)或“实现” (如在接口中)。
 
限定类型参数格式:
 //单个限定
 //多重限定

举例如下:

public class Teacher{
    private T age;

    public T getAge() {
        return age;
    }
    public void setAge(T t) {

    }
}
Teacher teacher1 =new Teacher<>();//报错 因为已经限定了类型必须是Number及其子类
teacher1.setAge("张三");
//正确用法
Teacher teacher2 =new Teacher<>();
teacher2.setAge(20);

6,通配符

在通用代码中,称为通配符的问号( ? )表示未知类型。通配符可以在多种情况下使用:作为参数,字 段或局部变量的类型;有时作为返回类型(尽管更具体的做法是更好的编程习惯)。通配符从不用作泛 型方法调用,泛型类实例创建或超类型的类型参数。

 

①上限通配符

格式如下:


//例如:
 
//其中,Foo可以是任何类型,匹配Foo和Foo的任何子类型

下面来看一个具体的使用案例:

public class Fruit {

    @Override
    public String toString() {
        return "水果";
    }
}
public class Apple extends Fruit {

    @Override
    public String toString() {
        return "苹果";
    }
}
ArrayList fruits =new ArrayList();//这样会在编译期报错

这样为什么会报错呢?Java不是可以将子类对象赋值给一个父类对象的引用吗?

下来再来看一下这段代码:

public class Banana extends Fruit {

    @Override
    public String toString() {
        return "香蕉";
    }
}
ArrayList apples =new ArrayList();
Apple apple =new Apple();
apples.add(apple);
//----------------上面的代码不会报错 是正常逻辑------------
ArrayList fruits =apples;//假如这行代码在编译期不报错的话
fruits.add(new Banana());
Apple a =fruits.get(1);//这行代码在运行期间就会报类型转换异常的错误

所以泛型是不支持这种向上转型的。

但是如果我们一定要这么写呢?也不是不可以,用通配符就可以做到

ArrayList fruits =new ArrayList();//编译期不会报错
Apple apple =new Apple();
fruits.add(apple);//但是在添加的时候会在编译期报错

大家想一想为什么会在添加的时候会在编译期报错呢?

因为如果放开的话,我还是同样的可以添加香蕉,苹果等子类,这样在运行期间就可能会出现更大的错误,所以编译器直接就不让你添加了,这样就不会有问题了。

但是这样做有什么意义呢?

当你只想让用户往外取值,不想让用户进行写入时,的意义就体现出来了,简而言之就是只能读取不能写入

②下限通配符

格式如下:


//例如:

//其中,Zoo可以是任何类型,匹配Zoo和Zoo的任何超类

下面来看一个例子:

ArrayList fruits =new ArrayList();//会在编译期报错
Apple apple =new Apple();
fruits.add(apple);

为什么会在编译期报错呢?同上限通配符一样的道理,泛型也不支持这种转型

那么需要怎么做呢?请看下面:

ArrayList fruits =new ArrayList();//不报错 正常运行
Apple apple =new Apple();
fruits.add(apple);
Object object = fruits.get(0);

使用下限通配符为什么可以添加呢? 因为往里面添加的都是apple的父类,其归根结底,都会用一个最终的父类表示,所以不会有问题。但是往外读的时候,我不知道是苹果还是水果,所以不建议读取,虽然可以用Object接收。

简而言之,与上限通配符相反,下线通配符只能写入,不能读取

③通配符使用准则

在开发中,我们应该在什么时候使用上限通配符和下限通配符呢?

输入 变量:输入变量将数据提供给代码。一个具有两个参数的复制方法: copy(src, dest) 。
src参数提供要复制的数据,因此它是输入参数。
输出 变量:输出变量保存要在其它地方使用的数据。在复制示例 copy(src, dest) 中,dest参数接
受数据,因此它是输出参数。
 
通配符准则:
使用上限通配符定义输入变量,使用 extends 关键字。
使用下限通配符定义输出变量,使用 super 关键字。
如果可以使用 Object 类中定义的方法访问输入变量,请使用无界通配符( ? )。
如果代码需要同时使用输入和输出变量来访问变量,则不要使用通配符。
这些准则不适用于方法的返回类型。应该避免使用通配符作为返回类型
 

 

7,类型擦除

Java语言引入了泛型,以在编译时提供更严格的类型检查并支持泛型编程。 为了实现泛型,Java编译器将类型擦除应用于:
1,如果类型参数不受限制,则将通用类型中的所有类型参数替换为其边界(上下限)或 Object 。因此,产生的字节码仅包含普通的类,接口和方法。
2,必要时插入类型转换,以保持类型安全。
3,生成桥接方法以在扩展的泛型类型中保留多态。
类型擦除可确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。
在类型擦除过程中,Java编译器将擦除所有类型参数,如果类型参数是有界的,则将每个参数替换为其第一个边界;如果类型参数是无界的,则将其替换为 Object 。

 

下面来看几个例子:

public class Node {
    private T data;
    private Node next;
    public Node(T data, Node next) {
        this.data = data;
        this.next = next;
    }
    public T getData() { return data; }
    
}
由于类型参数T是无界的,因此Java编译器将其替换为 Object :
 
public class Node {
    private Object data;
    private Node next;
    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }
    public Object getData() { return data; }
}
在下面的示例中,通用Node类使用限定类型参数:
 
public class Node> {
    private T data;
    private Node next;
    public Node(T data, Node next) {
        this.data = data;
        this.next = next;
    }
    public T getData() { return data; }
}
Java编译器将绑定类型参数T替换为第一个绑定类 Comparable :
 
public class Node {
    private Comparable data;
    private Node next;
    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }
    public Comparable getData() { return data; }
}

 

8,对泛型的限制

①无法实例化具有基本类型的泛型类型

请看下面例子:

class Pair {
    private K key;
    private V value;
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
}

创建对对象时,不能用基本类型替换类型参数K或V:

Pair p = new Pair<>(8, 'a');//编译错误

你只能将非基本类型替换为类型参数K和V:

Pair p = new Pair<>(8, 'a'); 

 

②无法创建类型参数的实例

例如,以下代码会导致编译时错误:

public static  void append(List list) {
    E elem = new E(); //编译期错误
    list.add(elem);
}

解决方法是,可以通过反射创建类型参数的对象:

public static  void append(List list, Class cls) throws Exception {
    E elem = cls.newInstance(); // OK
    list.add(elem);
}

 

③无法声明类型为类型参数的静态字段

类的静态字段是该类的所有非静态对象共享的类级别变量。因此,不允许使用类型参数的静态字段

public class MobileDevice {
    private static T os;
}

 

④无法将Casts或instanceof与参数化类型一起使用

因为Java编译器会擦除通用代码中的所有类型参数,所以你无法验证在运行时使用的是通用类型的参数 化类型
public static  void rtti(List list) {
    if (list instanceof ArrayList) { // 编译器错误
    // ...
    }
}

 

⑤无法创建参数化类型的数组

List[] arrayOfLists = new List[2];//编译期报错

 

 

⑥无法创建,捕获或抛出参数化类型的对象

泛型类不能直接或间接扩展 Throwable 类。
class MathException extends Exception { /* ... */ } // 编译期错误
class QueueFullException extends Throwable { /* ... */ }// 编译器错误
方法无法捕获类型参数的实例:
public static  void execute(List jobs) {
    try {
        for (J job : jobs)
    } catch (T e) { //编译期错误

    }
}
但是,你可以在 throws 子句中使用类型参数:
class Parser {
    public void parse(File file) throws T { // OK
    }
}

 

⑦无法重载每个重载的形式参数类型都擦除为相同原始(raw)类型的方法

一个类不能有两个重载的方法,这些方法在类型擦除后将具有相同的签名。
public class Example {
    public void print(Set strSet) { }
    public void print(Set intSet) { }
}

 

 

9,在安卓中的使用

那么在了解了泛型的知识之后,我们在安卓开发中,到底用到了哪些泛型呢?

①网络请求

最常用的地方,就是解析http请求下来的json数据。因为http请求下来的数据,我们不知道需要转换成什么类型,只有在调用的地方才知道,所以我们采用泛型。

先定义一个回调接口:

public interface CallBack {

    void  onError(Exception e);

    void onSuccess(T t);
}

在实际请求中传入匿名内部类: 

 OmniHttp.get(getHOST() + "/app-wn/login")
        .syncRequest(true)
        .execute(new CallBack() {
            @Override
            public void onError(ApiException e) {
                       
            }

            @Override
            public void onSuccess(LoginBean loginBean) {
                        
            }
});

这只是简单的写了一个小例子,可以尝试在自己写一下

②findViewById()

 /**
 * FindViewById的源码,泛型封装,减少强转代码
 */
 public  T findViewById(@IdRes int id) {
     return getDelegate().findViewById(id);
 }
//使用前
ImageView item = (ImageView)findViewById(R.id.item);
//使用后
ImageView item =findViewById(R.id.item);

③BaseAdapter实现封装的Adapter

有时间补充

④MVP架构

有时间补充

10,总结

花了一天时间整理了下泛型的相关知识点,在android中的应用后续还会继续补充。

 

 

你可能感兴趣的:(android)