前言
泛型是什么
generic,中文意思是通用的、一类的,结合其应用场景,我理解泛型是一种
通用类型。但我们一般指泛型都是指其实现方式,也就是
将类型参数化 public static void quickSort(int[] data, int start, int end) {
        int key = data[start];
        int i = start;
        int j = end;
        while (i < j) {
            while (data[j] > key && j > i) {
                j--;
            }
            data[i] = data[j];
            while (data[i] < key && i < j) {
                i++;
            }
            data[j] = data[i];
        }
        data[i] = key;
        if (i - 1 > start) {
            quickSort(data, start, i - 1);
        }
        if (i + 1 < end) {
            quickSort(data, i + 1, end);
        }
    }
    public static  <T extends Comparable<T>> void quickSort(T[] data, int start, int end) {
        T key = data[start];
        int i = start;
        int j = end;
        while (i < j) {
            while (data[j].compareTo(key) > 0 && j > i) {
                j--;
            }
            data[i] = data[j];
            while (data[i].compareTo(key) < 0 && i < j) {
                i++;
            }
            data[j] = data[i];
        }
        data[i] = key;
        if (i - 1 > start) {
            quickSort(data, start, i - 1);
        }
        if (i + 1 < end) {
            quickSort(data, i + 1, end);
        }
    }
- 
当参数类型不明确,可能会扩展为多种时。 
 - 
想声明参数类型为 
Object
,并在使用时用
instanceof
判断时。
 
泛型只能替代Object的子类型,如果需要替代基本类型,可以使用包装类,至于为什么,会在下文中说明。
使用
声明
<占位符 [,另一个占位符] >的形式,需要在一个地方同时声明多个占位符时,使用
,隔开。占位符的格式并无限制,不过一般约定使用单个大写字母,如 T 代表类型(type),E 代表元素*(element)等。虽然没有严格规定,不过为了代码的易读性,最好使用前检查一下约定用法。
    class Generics<T> { // 在类名后声明引入泛型类型
        private T field;  // 引入后可以将字段声明为泛型类型
        public T getField() { // 类方法内也可以使用泛型类型
            return field;
        }
    }
把泛型声明在方法上时:
    public [static] <T> void testMethod(T arg) { // 访问限定符[静态方法在 static] 后使用 <占位符> 声明泛型方法后,在参数列表后就可以使用泛型类型了
        // doSomething
    }
最后是在接口中声明泛型,如上面的快排中,我们使用了 Comparable<T> 的泛型接口,与此类似的还有 Searializable<T> Iterable<T>等,其实在接口中声明与在类中声明并没有什么太大区别。
调用
然后是泛型的调用,泛型的调用和普通方法或类的调用没有什么大的区别,如下:
    public static void main(String[] args) {
        String[] strArr = new String[2];
        // 泛型方法的调用跟普通方法相同
  Generics.quickSort(strArr, 0, 30 );
  // 泛型类在调用时需要声明一种精确类型
        Generics<Long> sample = new Generics<>();
        Long field = sample.getField();
    }
    // 泛型接口需要在泛型类里实现
    class GenericsImpl<T> implements Comparable<T> {
    @Override
    public int compareTo(T o) {
        return 0;
    }
}
类型擦除
由来
Generics<Long>被擦除后是
Generics,我们常用的
List<String>被擦除后只剩下
List。
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        List<Long> longList = new ArrayList<>();
        if (stringList.getClass() == longList.getClass()) {
            System.out.println(stringList.getClass().toString());
            System.out.println(longList.getClass().toString());
   System.out.println("type erased");
        }
    }
class java.util.ArrayList,两者类型相同,说明其泛型类型被擦除掉了。
signature字段,其中指向了常量表中泛型的真正类型,所以泛型的真正类型,还可以通过反射获取得到。
实现
javac命令编译成 class 文件后,再使用
javap命令查看其字节码信息:

T被替换成了 Object 类型,而在
main方法里
getField字段时,进行了类型转换(
checkcast),如此,我们可以看出来 Java 的泛型实现了,一段泛型代码的编译运行过程如下:
- 
编译期间编译器检查传入的泛型类型与声明的泛型类型是否匹配,不匹配则报出编译器错误; 
 - 
编译器执行类型擦除,字节码内只保留其原始类型; 
 - 
运行期间,再将 Object 转换为所需要的泛型类型。 
 
Java 的泛型实际上是由编译器实现的,将泛型类型转换为 Object 类型,在运行期间再进行状态转换。
实践问题
具体类型须为Object子类型
Generics<int> generics = new Generics<int>();在编译期间就会报错的。
边界限定通配符的使用
<? extends Generics>是上边界限定通配符,避开
上边界这个比较模糊的词不谈,我们来看其声明
xx extends Generics, XX 是继承了 Generics 的类(也有可能是实现,下面只说继承),我们按照以下代码声明:
List<? extends Generics> genericsList = new ArrayList<>();
Generics generics = genericsList.get(0);
genericsList.add(new Generics<String>()); // 编译无法通过
中取出来的类一定是可以转换为 Generics,所以 get 方法没问题;而具体是什么类,我们并不知道,将父类强制转换成子类可能会造成运行期错误,所以编译器不允许这种情况;
<? super Generics>是下边界限定通配符, XX 是 Generics 的父类,所以:
List<? super Generics> genericsList = new ArrayList<>();
genericsList.add(new Generics()); // 编译无法通过
Generics generics = genericsList.get(0);
最佳实践
- 
将代码逻辑拆分为两部分:通用逻辑和类型相关逻辑;通用逻辑是一些跟参数类型无关的逻辑,如快排的元素位置整理等;类型相关逻辑,顾名思义,是需要确定类型后才能编写的逻辑,如元素大小的比较,String 类型的比较和 int 类型的比较就不一样。 
 - 
如果没有类型相关的逻辑,如 List 作为容器不需要考虑什么类型,那么直接完善通用代码即可。 
 - 
如果有参数类型相关的逻辑,那么就需要考虑这些逻辑是否已有共同的接口实现,如果已有共同的接口实现,可以使用边界限定通配符。如快排的元素就实现了 
Compare
接口,Object 已经实现了
toString()
方法,所有的打印语句都可以调用它。
 - 
如果还没有共同的接口,那么需要考虑是否可以抽象出一个通用的接口实现,如打印人类的衣服颜色和动物的毛皮颜色,就可以抽象出一个 
getColor()
接口,抽象之后再使用边界限定通配符。
 - 
如果无法抽象出通用接口,如输出人类身高或动物体重这种,还是不要使用泛型了,因为不限定类型的话,具体类型的方法调用也就无从谈起,编译也无法通过。 
 

小结
来源 | https://zhenbianshu.github.io/
   
   
   
 
    
    
    
   
   
    
   
   
   
 
    
    
    
  
     
     
     推荐阅读
 
    
    
     
   
   
    
   
   
   
 
    
    
    1. 
 
    
    
    GitHub 上有什么好玩的项目?
   
   
    
   
   
   
 
    
    
    2. 这个牛逼哄哄的数据库开源了
   
   
    
   
   
   
 
    
    
    3.
 
    
    
     SpringSecurity + JWT 实现单点登录
   
   
    
   
   
   
 
    
    
    4. 100 道 Linux 常见面试题
   
   
    
本文分享自微信公众号 - Java后端(web_resource)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
相关文章
暂无评论...

