精华内容
下载资源
问答
  • Java的泛型(参数化类型

    万次阅读 多人点赞 2018-05-30 20:09:54
    泛型的本质是为了参数化类型(在创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,...

    平时工作中泛型用到的比较多,但是并没有对泛型有更进一步的了解,所以最近看了很多的资料,这里也进行一个总结和汇总。

    泛型是Java中一个非常重要的特性,在各种面向对象的编程、设计模式、开源框架和Java集合中都有非常广泛的应用。

    本文参考:java 泛型详解Java中的泛型方法、 java泛型详解java 泛型详解

    1、泛型概念的提出

    Java语言类型包括八种基本类型(byte short int long float double boolean char)和复杂类型,复杂类型包括类和数组。
    早期Java版本(1.4之前)如果要代指某个泛化类对象,只能使用Object,这样写出来的代码需要增加强转,而且缺少类型检查,代码缺少健壮性。在1.5之后,Java引入了泛型(Generic)的概念,提供了一套抽象的类型表示方法。利用泛型,我们可以:
    1、表示多个可变类型之间的相互关系:HashMap<T,S>表示类型T与S的映射,HashMap<T, S extends T>表示T的子类与T的映射关系

    2、细化类的能力:ArrayList<T> 可以容纳任何指定类型T的数据,当T代指人,则是人的有序列表,当T代指杯子,则是杯子的有序列表,所有对象个体可以共用相同的操作行为

    3、复杂类型被细分成更多类型:List<People>和List<Cup>是两种不同的类型,这意味着List<People> listP = new ArrayList<Cup>()是不可编译的。后面会提到,这种检查基于编译而非运行,所以说是不可编译并非不可运行,因为运行时ArrayList不保留Cup信息。另外要注意,即使People继承自Object,List<Object> listO = new ArrayList<People>()也是不可编译的,应理解为两种不同类型。因为listO可以容纳任意类型,而实例化的People列表只能接收People实例,这会破坏数据类型完整性。

    看一段代码:

    public class GenericsDemo {
        public static void main(String[] args) {
            List list = new ArrayList();
            list.add("str1");
            list.add("test");
            list.add(100);
    
            for (int i = 0; i < list.size(); i++) {
                String name = (String) list.get(i); // java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
                System.out.println("name:" + name);
            }
        }
    }

    定义了一个List类型的集合,先向其中加入了两个字符串类型的值,随后加入一个Integer类型的值。这是完全允许的,因为此时list默认的类型为Object类型。在之后的循环中,由于忘记了之前在list中也加入了Integer类型的值或其他编码原因,很容易出现类似例子中ClassCastException的错误。因为编译阶段正常,而运行时会出现“java.lang.ClassCastException”异常。因此,导致此类错误编码过程中不易发现。

    在如上的编码过程中,我们发现主要存在两个问题:

    1、当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,改对象的编译类型变成了Object类型,但其运行时类型任然为其本身类型。

    2、因此,//1处取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现“java.lang.ClassCastException”异常。

    那么有没有什么办法可以使集合能够记住集合内元素各类型,且能够达到只要编译时不出现问题,运行时就不会出现“java.lang.ClassCastException”异常呢?答案就是使用泛型。

    2、什么是泛型?

    泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

    泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

    看起来有点绕,我们还是用上面的例子,现在采用泛型的写法:

        public static void main(String[] args) {
            List<String> list = new ArrayList<String>();
            list.add("str1");
            list.add("test");
    //        list.add(100);  //编译阶段这里就会报错
    
            for (int i = 0; i < list.size(); i++) {
                String name = list.get(i);
                System.out.println("name:" + name);
            }
        }

    采用泛型写法后,想加入一个Integer类型的对象时会出现编译错误,通过List<String>,直接限定了list集合中只能含有String类型的元素,从而在从list里面Get数据的时候无须进行强制类型转换,因为此时,集合能够记住元素的类型信息,编译器已经能够确认它是String类型了。

    3、特性

    泛型只在编译阶段有效。看下面的代码:

            List<String> stringArrayList = new ArrayList<String>();
            List<Integer> integerArrayList = new ArrayList<Integer>();
    
            Class classStringArrayList = stringArrayList.getClass();
            Class classIntegerArrayList = integerArrayList.getClass();
    
            if(classStringArrayList.equals(classIntegerArrayList)){
                System.out.print("输入结果:类型相同");
            }
    输入结果:类型相同

    通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

    总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

    4、泛型的使用

    泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。

    泛型类

    泛型类用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

    泛型类的最基本写法(这么看可能会有点晕,会在下面的例子中详解):

    class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
      private 泛型标识 /*(成员变量类型)*/ var; 
      .....
    
      }
    }

    下面是一个最简单的自定义泛型类

    //此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
    //在实例化泛型类时,必须指定T的具体类型
    public class BOX<T>{ 
        //key这个成员变量的类型为T,T的类型由外部指定  
        private T key;
    
        public Box(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
            this.key = key;
        }
    
        public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
            return key;
        }
    }
            //泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
            //传入的实参类型需与泛型的类型参数类型相同,即为Integer.
            Box<Integer> boxInteger = new Box<Integer>(123456);
    
            //传入的实参类型需与泛型的类型参数类型相同,即为String.
            Box<String> boxString = new Box<String>("key_vlaue");
            System.out.println("泛型测试: key is " + boxInteger.getData());
            System.out.println("泛型测试: key is " + boxString.getData());
    泛型测试: key is 123456
    泛型测试: key is key_vlaue

    定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。下面的例子编译没有问题,获取对象的value也正确。

        Box box = new Box("string");
        Box box1 = new Box(1234);
        Box box2 = new Box(12.34);

    泛型接口

    泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:

    //定义一个泛型接口
    public interface Generator<T> {
        //接口方法
        public T next();
    }

    当实现泛型接口的类,未传入泛型实参时:

    /**
     * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
     * 即:class FruitGenerator<T> implements Generator<T>{
     * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
     */
    class FruitGenerator<T> implements Generator<T>{
        @Override
        public T next() {
            return null;
        }
    }

    当实现泛型接口的类,传入泛型实参时:

    /**
     * 传入泛型实参时:
     * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
     * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
     * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
     * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
     */
    public class FruitGenerator implements Generator<String> {
    
        private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
    
        @Override
        public String next() {
            Random rand = new Random();
            return fruits[rand.nextInt(3)];
        }
    }

    泛型通配符

    我们知道IngeterNumber的一个子类,同时在特性章节中我们也验证过Generic<Ingeter>Generic<Number>实际上是相同的一种基本类型。那么问题来了,在使用Generic<Number>作为形参的方法中,能否使用Generic<Ingeter>的实例传入呢?在逻辑上类似于Generic<Number>Generic<Ingeter>是否可以看成具有父子关系的泛型类型呢?

    为了弄清楚这个问题,我们使用Generic<T>这个泛型类继续看下面的例子:

    public void showKeyValue1(Generic<Number> obj){
        System.out.println("泛型测试:key value is " + obj.getKey());
    }
    Generic<Integer> gInteger = new Generic<Integer>(123);
    Generic<Number> gNumber = new Generic<Number>(456);
    
    showKeyValue(gNumber);
    
    // showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer> 
    // cannot be applied to Generic<java.lang.Number>
    // showKeyValue(gInteger);

    通过提示信息我们可以看到Generic<Integer>不能被看作为`Generic<Number>的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的

    回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Generic<Integer>类型的类,这显然与java中的多台理念相违背。因此我们需要一个在逻辑上可以表示同时Generic<Integer>Generic<Number>父类的引用类型。由此类型通配符应运而生。

    我们可以将上面的方法改一下:

    public void showKeyValue1(Generic<?> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }

    类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。重要说三遍!此处’?’是类型实参,而不是类型形参 ! 此处’?’是类型实参,而不是类型形参 !再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。

    可以解决当具体类型不确定的时候,这个通配符就是 ?  ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。

    泛型方法

    泛型方法,是在调用方法的时候指明泛型的具体类型 。泛型类的定义非常简单,但是泛型方法就比较复杂了。

    /**
     * 泛型方法的基本介绍
     * @param tClass 传入的泛型实参
     * @return T 返回值为T类型
     * 说明:
     *     1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
     *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
     *     3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
     *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
     */
    public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
      IllegalAccessException{
            T instance = tClass.newInstance();
            return instance;
    }
    Object obj = genericMethod(Class.forName("com.test.test"));
    泛型方法的使用

    光看上面的例子有的同学可能依然会非常迷糊,我们再通过一个例子,把我泛型方法再总结一下。

    public class GenericTest {
       //这个类是个泛型类,在上面已经介绍过
       public class Generic<T>{     
            private T key;
    
            public Generic(T key) {
                this.key = key;
            }
    
            //我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
            //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
            //所以在这个方法中才可以继续使用 T 这个泛型。
            public T getKey(){
                return key;
            }
    
            /**
             * 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
             * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
            public E setKey(E key){
                 this.key = keu
            }
            */
        }
    
        /** 
         * 这才是一个真正的泛型方法。
         * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
         * 这个T可以出现在这个泛型方法的任意位置.
         * 泛型的数量也可以为任意多个 
         *    如:public <T,K> K showKeyName(Generic<T> container){
         *        ...
         *        }
         */
        public <T> T showKeyName(Generic<T> container){
            System.out.println("container key :" + container.getKey());
            //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
            T test = container.getKey();
            return test;
        }
    
        //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
        public void showKeyValue1(Generic<Number> obj){
            Log.d("泛型测试","key value is " + obj.getKey());
        }
    
        //这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
        //同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
        public void showKeyValue2(Generic<?> obj){
            Log.d("泛型测试","key value is " + obj.getKey());
        }
    
         /**
         * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
         * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
         * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
        public <T> T showKeyName(Generic<E> container){
            ...
        }  
        */
    
        /**
         * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
         * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
         * 所以这也不是一个正确的泛型方法声明。
        public void showkey(T genericObj){
    
        }
        */
    
        public static void main(String[] args) {
    
    
        }
    }
    类中的泛型方法

    当然这并不是泛型方法的全部,泛型方法可以出现杂任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下

    public class GenericFruit {
        class Fruit{
            @Override
            public String toString() {
                return "fruit";
            }
        }
    
        class Apple extends Fruit{
            @Override
            public String toString() {
                return "apple";
            }
        }
    
        class Person{
            @Override
            public String toString() {
                return "Person";
            }
        }
    
        class GenerateTest<T>{
            public void show_1(T t){
                System.out.println(t.toString());
            }
    
            //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
            //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
            public <E> void show_3(E t){
                System.out.println(t.toString());
            }
    
            //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
            public <T> void show_2(T t){
                System.out.println(t.toString());
            }
        }
    
        public static void main(String[] args) {
            Apple apple = new Apple();
            Person person = new Person();
    
            GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
            //apple是Fruit的子类,所以这里可以
            generateTest.show_1(apple);
            //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
            //generateTest.show_1(person);
    
            //使用这两个方法都可以成功
            generateTest.show_2(apple);
            generateTest.show_2(person);
    
            //使用这两个方法也都可以成功
            generateTest.show_3(apple);
            generateTest.show_3(person);
        }
    }
    泛型方法与可变参数

    再看一个泛型方法和可变参数的例子:

    public <T> void printMsg( T... args){
        for(T t : args){
            Log.d("泛型测试","t is " + t);
        }
    }
    printMsg("111",222,"aaaa","2323.4",55.55);
    静态方法与泛型

    静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

    即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。

    public class StaticGenerator<T> {
        ....
        ....
        /**
         * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
         * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
         * 如:public static void show(T t){..},此时编译器会提示错误信息:
              "StaticGenerator cannot be refrenced from static context"
         */
        public static <T> void show(T t){
    
        }
    }
    泛型方法总结

    泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:

    无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。

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

    为泛型添加上边界,即传入的类型实参必须是指定类型的子类型

    public void showKeyValue1(Generic<? extends Number> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }
    Generic<String> generic1 = new Generic<String>("11111");
    Generic<Integer> generic2 = new Generic<Integer>(2222);
    Generic<Float> generic3 = new Generic<Float>(2.4f);
    Generic<Double> generic4 = new Generic<Double>(2.56);
    
    //这一行代码编译器会提示错误,因为String类型并不是Number类型的子类
    //showKeyValue1(generic1);
    
    showKeyValue1(generic2);
    showKeyValue1(generic3);
    showKeyValue1(generic4);
    如果我们把泛型类的定义也改一下:
    public class Generic<T extends Number>{
        private T key;
    
        public Generic(T key) {
            this.key = key;
        }
    
        public T getKey(){
            return key;
        }
    }
    //这一行代码也会报错,因为String不是Number的子类
    Generic<String> generic1 = new Generic<String>("11111");

    再来一个泛型方法的例子:

    //在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加
    //public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound"
    public <T extends Number> T showKeyName(Generic<T> container){
        System.out.println("container key :" + container.getKey());
        T test = container.getKey();
        return test;
    }

    通过上面的两个例子可以看出:泛型的上下边界添加,必须与泛型的声明在一起 。

    关于“泛型数组”

    看到了很多文章中都会提起泛型数组,经过查看sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组”的。

    也就是说下面的这个例子是不可以的:

    List<String>[] ls = new ArrayList<String>[10];  

    而使用通配符创建泛型数组是可以的,如下面这个例子:

    List<?>[] ls = new ArrayList<?>[10];  
    这样也是可以的:
    List<String>[] ls = new ArrayList[10];

    下面使用Sun的一篇文档的一个例子来说明这个问题:

    List<String>[] lsa = new List<String>[10]; // Not really allowed.    
    Object o = lsa;    
    Object[] oa = (Object[]) o;    
    List<Integer> li = new ArrayList<Integer>();    
    li.add(new Integer(3));    
    oa[1] = li; // Unsound, but passes run time store check    
    String s = lsa[1].get(0); // Run-time error: ClassCastException.

    这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。

    而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。

    下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。

    List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.    
    Object o = lsa;    
    Object[] oa = (Object[]) o;    
    List<Integer> li = new ArrayList<Integer>();    
    li.add(new Integer(3));    
    oa[1] = li; // Correct.    
    Integer i = (Integer) lsa[1].get(0); // OK 

    5、泛型的实现原理

    1、Java泛型是编译时技术,在运行时不包含类型信息,仅其实例中包含类型参数的定义信息。
    2、Java利用编译器擦除(erasure,前端处理)实现泛型,基本上就是泛型版本源码到非泛型版本源码的转化。
    3、擦除去掉了所有的泛型类内所有的泛型类型信息,所有在尖括号之间的类型信息都被扔掉.
    举例来说:List<String>类型被转换为List,所有对类型变量String的引用被替换成类型变量的上限(通常是Object)。

    而且,无论何时结果代码类型不正确,会插入一个到合适类型的转换。

    public <T> T badCast(T t, Object o) {  
    return (T) o; // unchecked warning  
    } 
    这说明String类型参数在List运行时并不存在。它们也就不会添加任何的时间或者空间上的负担。但同时,这也意味着你不能依靠他们进行类型转换。

    4、一个泛型类被其所有调用共享
    对于上文中的GenericClass,在编译后其内部是不存入泛型信息的,也就是说:
    GenericClass<AClass> gclassA = new GenericClass<AClass>();  
    GenericClass<BClass> gclassB = new GenericClass<BClass>();  
    gClassA.getClass() == gClassB.getClass()  

     

    这个判断返回的值是true,而非false,因为一个泛型类所有实例运行时具有相同的运行时类,其实际类型参数被擦除了。
    那么是不是GenericClass里完全不存AClass的信息呢?这个也不是,它内部存储的是泛型向上父类的引用,比如:
    GenericClass<AClass extends Charsequence>, 其编译后内部存储的泛型替代是Charsequence,而不是Object。

    那么我们编码时的泛型的类型判断是怎么实现的呢?
    其实这个过程是编译时检查的,也就是说限制gClassA.add(new BClass()) 这样的使用的方式的主体,不是运行时代码,而是编译时监测。

    泛型的意义就在于,对所有其支持的类型参数,有相同的行为,从而可以被当作不同类型使用;类的静态变量和方法在所有实例间共享使用,所以不能使用泛型。

    5、泛型与instanceof
    泛型擦除了类型信息,所以使用instanceof检查某个实例是否是特定类型的泛型类是不可行的:
    GenericClass genericClass = new GenericClass<String>();
    if (genericClass instanceof GenericClass<String>) {} // 编译错误
    同时:
    GenericClass<String> class1 = (GenericClass<String>) genericClass; //会报警告

    6、最后

    本文中的例子主要是为了阐述泛型中的一些思想而简单举出的,并不一定有着实际的可用性。另外,一提到泛型,相信大家用到最多的就是在集合中,其实,在实际的编程过程中,自己可以使用泛型去简化开发,且能很好的保证代码质量。

    展开全文
  • JMeter参数化4种实现方式

    千次阅读 多人点赞 2020-08-26 21:09:44
    前言 下面是目前JMeter系列已有博文...1 参数化释义 什么是参数化?从字面上去理解的话,就是事先准备好数据(广义上来说,可以是具体的数据值,也可以是数据生成规则),而非在脚本中写死,脚本执行时从准备好的数据中

    前言

    下面是目前JMeter系列已有博文列表,持续更新中:

    1. JMeter安装与接口测试入门
    2. JMeter接口测试之断言实现
    3. JMeter参数化实现方式
    4. JMeter配置元件
    5. JMeter操作Mysql数据库
    6. BeanShell Sampler与BeanShell断言
    7. JMeter Linux下执行测试
    8. JMeter自定义日志与日志分析

    1 参数化释义

    什么是参数化?从字面上去理解的话,就是事先准备好数据(广义上来说,可以是具体的数据值,也可以是数据生成规则),而非在脚本中写死,脚本执行时从准备好的数据中取值。

    参数化:是自动化测试脚本的一种常用技巧,可将脚本中的某些输入使用参数来代替,如登录时利用GET/POST请求方式传递参数的场景,在脚本运行时指定参数的取值范围和规则。
    脚本在运行时,根据需要选取不同的参数值作为输入,该方式称为数据驱动测试(Data Driven Test),而参数的取值范围被称为数据池(Data Pool)。

    JMeter提供了多种参数化方式,下面就其中常用的4种展开阐述。

    方式适用场景
    CSV Data Set Config我们通常所指的参数化。数据存储在文件中,参数化取值范围大,灵活性强
    User Parameter适用于参数取值范围很小时
    函数助手_Random等函数,生成随机数字和随机字符串实现参数化
    User Defined Variables用户自定义变量,更多用于设置全局变量

    2 参数化实现

    2.1 CSV Data Set Config

    在JMeter中提起参数化,我们默认就想到CSV Data Set Config(以下简称CSV),CSV能够读取文件中的数据并生成变量,被JMeter脚本引用,从而实现参数化。下面我们来详细探究一下。

    CSV简介

    线程组右键–>添加–>配置元件–>CSV Data Set Config,就创建了一个CSV,界面是这个样子的:
    在这里插入图片描述
    各项参数详解如下:

    参 数描 述必 须
    Name脚本中显示的这个元件的描述性名称
    Filename文件名。待读取文件的名称。可以写入绝对路径,也可以写入相对路径(相对于bin目录),如果直接写文件名,则该文件要放在bin目录中。对于分布式测试,主机和远程机中相应目录下应该有相同的CSV文件
    File Encoding文件编码。文件读取时的编码格式,不填则使用操作系统的编码格式
    Variable Names变量名称。多个变量名之间必须用分隔符分隔。如果该项为空,则文件首行会被读取并解析为列名列表
    Ignore first line是否忽略首行?如果csv文件中没有表头,则选择false
    Delimiter分隔符。将一行数据分隔成多个变量,默认为逗号,也可以使用“\t”。如果一行数据分隔后的值比Vairable Names中定义的变量少,这些变量将保留以前的值(如果有值的话)
    Allow quoted data?是否允许变量使用双引号?允许的话,变量将可以括在双引号内,并且这些变量名可以包含分隔符
    Recycle on EOF?遇到文件结束符是否再次循环?默认为 true
    Stop thread on EOF?遇到文件结束符是否停止线程?默认为 true
    Recycle on EOF?当Recycle on EOF为False时,停止线程,当Recycle on EOF为True时,此项无意义,默认为 false
    Sharing mode线程共享模式。1、All threads(默认):一个线程组内,各个线程(用户)唯一顺序取值;2、current thread:一个线程组内,各个线程(用户)各自顺序取值;3、线程组各自独立,但每个线程组内各个线程(用户)唯一顺序取值;

    需要着重说明一下的是Sharing mode,也就是线程共享模式。线程共享模式,是指多个线程对文件数据取值顺序模式,JMeter提供了3种模式:

    1. All threads:所有线程。如果脚本有多个线程组,在这种模式下,各线程组的所有线程也要依次唯一顺序取值。例如,脚本有2个线程组,各有2个线程,文件内有5行数据,脚本运行时,将如下图一样循环往复取值:
      在这里插入图片描述

    2. Current thread group:当前线程组。各个线程组之间隔离,线程组内的线程顺序唯一取值。
      在这里插入图片描述

    3. Current thread:当前线程。这种模式下,每个线程独立,顺序唯一取值。
      在这里插入图片描述

    CSV实例

    下面看一个实例。首先有userInfo.txt的文件,放置在bin目录中,内容如下:
    在这里插入图片描述
    在这里插入图片描述

    • 文件名:文件在bin目录中,使用相对目录
    • 变量名称:两列数据分别属于mobile和password两个变量
    • 分隔符:以逗号分割

    在HTTP请求中引用CSV生成的变量的方式是${变量名}的方式:

    在这里插入图片描述
    运行脚本,察看结果树:

    在这里插入图片描述
    可以看到,文件中的数据被脚本成功引用。JMeter使用CSV实现参数化就是这么简单。

    注意事项

    CSV使用中最常见的一个问题是文件路径不对。当遇到这种问题时,因为运行脚本没有明显提示,许多人遇到后会感觉很懵,不知道问题在哪。其实,仔细观察会发现右上角黄色三角处数字在增加,点击该区域便打开了日志,日志里记录了相应错误:File userInfo2.txt must exist and be readable,也就是提示参数化文件不存在或路径不可达。
    在这里插入图片描述

    2.2 User Parameters

    User Parameters,也就是用户参数,也能实现参数化。

    创建方式:HTTP请求上右键–>添加–>前置处理器(Pre processors)–>用户参数。

    通过【添加变量】添加mobile和password两个变量,通过【添加用户】添加3组数据:
    在这里插入图片描述
    在HTTP请求中引用参数化的数据:

    在这里插入图片描述
    运行脚本,设置好的数据被成功引用。

    在这里插入图片描述
    这种方式相对来说简易一些,数据范围有限,适用场景也少。而且,每个线程会一直使用一组数据。例如,设置4个线程并发,那么线程1使用用户_1的数据,线程2使用用户_2的数据,线程3使用用户_3的数据,线程4使用用户_1的数据,无论各个线程循环多少次。

    在这里插入图片描述

    2.3 用户定义的变量

    用户自定义的变量,也可以实现请求参数的参数化。

    创建方式:线程组上右键–>配置元件(config element)–>用户定义的变量。

    在这里插入图片描述
    如上图,用起来也很简单,添加变量名和相对应的值就可以了。后面引用变量和前面一样,使用${mobile}的方式。

    【用户定义的变量】一般并非用来做HTTP请求参数化,而是用来定义全局变量,比如参数化文件路径、host、url等。

    【用户定义的变量】创建在【线程组】上,则在线程组内生效,如果创建在【Test Plan】上,则对所有线程组生效。

    2.4 Random

    函数助手中的Random函数,

    创建方式:Tools–>函数助手对话框–>选择一个功能–>_Random:
    在这里插入图片描述
    上图中,生成了一个表达式:${__Random(8000,9000,)},我们用这个表达式替换想要参数化的变量值,例如下图中的price变量:

    在这里插入图片描述
    运行脚本,察看结果树,可以看到效果:
    在这里插入图片描述
    这种方式适用于值在一定区间无规律随机取值的变量参数化,例如价格、数量等,并不适用于有较强规则的变量进行参数化,例如手机号。

    展开全文
  • Jmeter 参数化实现

    千次阅读 多人点赞 2018-06-22 17:27:27
    在测试过程中,我们经常遇到需要根据需求动态操作数据的情况,常规的固定数据无法满足我们的需要,这个时候,我们可以通过jmeter 提供的参数化组件来实现动态的获取数据、改变数据。 Jmeter 中常用的参数化方式 ...

          在测试过程中,我们经常遇到需要根据需求动态操作数据的情况,常规的固定数据无法满足我们的需要,这个时候,我们可以通过jmeter 提供的参数化组件来实现动态的获取数据、改变数据。

    Jmeter 中常用的参数化方式

          jmeter 为我们提供了四种实现参数化的方式,分别是【CSV Data Set Config】(数据集配置)组件、【用户参数】组件、【用户定义的变量】组件和【函数】组件,四种方式都有各自的使用场景,互为补充。

    1.【CSV Data Set Config】(数据集配置)组件

          我们借助此组件可以实现动态的从外部CSV文件中获取数据,从而达到批量操作数据的目的。
    使用此组件的前提条件:我们需要有一个 CSV 格式的数据文件。

    CSV文件编写格式:
    	1.每一行代表一条数据;
    	2.各字段之间用英文逗号分隔开;
    	3.编码格式采用 utf-8。
    

    假设现在有一个 CSV 格式的数据文件csv_test.txt,该文件内容如下图所示:
    这里写图片描述

          要实现依次读取文件中的内容,并将内容中第一个字段的值作为关键字在百度中搜索的操作,我们可以这样做:
    1)在jmeter中依次创建【测试计划】、【线程组】和【HTTP 请求】,并添加查看结果的组件【查看结果树】如图:
    这里写图片描述
    2)添加【CSV Data Set Config】(数据集配置)组件:右击【线程组】—>【添加】—>【配置元件】—>【CSV Data Set Config】。
    3)配置参数

    • 设置线程组循环次数:选中【线程组】,在右边属性窗口中将【循环次数】设置成 与CSV 数据文件中数据条数相同,在本例中即为3。
      这里写图片描述
    • 设置 CSV Data Set Config 组件的参数
      • Filename:CSV 数据文件所在路径。

      • File encoding:CSV文件的编码格式,即 utf-8;

      • Variable Names(comma-delimited):给 CSV 数据文件中数据的每个字段取一个名字,作为调用的变量名;CSV 中每个字段都对应一个变量,每个变量用逗号隔开

      • Delimiter(use ‘\t’ for tab):与CSV 中的分隔符 保持一致,本例中即为英文逗号;
        这里写图片描述

      • 一般情况下,我们只需要设置这四个属性即可。

    4)使用变量

    • 变量名即为上一步骤中设置的变量名
    • 变量使用格式为${变量名}
    • 在 HTTP 请求 URL 路径中使用正确格式调用变量,如图。
      这里写图片描述
    • 为了结果更明确,在 HTTP 请求的命名中也调用此变量标注。

    5)执行,结果如下图:
    这里写图片描述
          结果中显示,我们依次从 CSV 数据文件中获取到了每条数据的第一个字段的值,并将其作为百度搜索的关键字执行了搜索。

    2.【用户参数】组件

          除了读取外部文件中的数据,我们还可以在jmeter中定义一些数据,实现批量操作数据的功能。但由于此操作不是那么的易用,所以数据量较大时,不建议使用此操作。
    下面我们来看一下具体是怎么操作的:
    1)这里我们先把上面添加的 CSV Data Set Config 组件禁用掉(右击该组件后选择【禁用】即可);
    这里写图片描述
    2)通过添加【用户参数】组件,用来配置数据:右击【测试计划】—>【添加】—>【前置处理器】—>【用户参数】,如图:
    这里写图片描述
    3)配置数据。我们可以看到用户参数面板如下图所示,主要使用的部分就是图中标注出来的三个部分:
    这里写图片描述

    • ①这里应该不陌生了,就是用来标注我们这部分的参数是给干什么用的,取个名字,见名知意,好让我们以后再看的时候不会迷糊。

    • ②这里我们看到有四个按钮,第一行的两个是用来操作变量的,第二行的两个是用来操作用户的。这里所谓的变量就是我们要设置的参数变量名,用来在后面调用的;而用户就是参数值,每一个用户对应一条数据。

    • ③ 点击【添加变量】后会在③中添加一行输入框,提供给我们输入变量名和变量值,输入后保存即可。点击【添加用户】后会在【用户_1】后面添加一列,表示可以多添加一条数据。

    • 我们还是用上面例子中的数据添加到这,如下图:
      这里写图片描述

    • 从上面的结构我们可以看出,【用户参数】组件是通过表格的形式来存储数据的,每个变量和变量值都需要我们手动去一条一条添加,可想而知,如果数据量比较大的情况下,我们还是用此方式操作的话,就会比较麻烦了。这种情况下,就建议使用第一种方式来批量操作数据了。

    4)更改线程组设置,让jmeter循环读取我们的数据:

    • 将线程组的【线程数】更改为与我们【用户参数】中用户数量相同;
    • 将线程组的【循环次数】更改回 1 次。

    5)调用参数的方式与上面的例子相同,在需要的地方使用"${变量名}"的格式调用即可,这次我们来遍历一下 【country】的值。
    这里写图片描述
    6)我们来执行一下,看看读取【用户参数】的执行结果:
    这里写图片描述
    出现上面的结果,就说明我们配置成功了!

    3.【用户定义的变量】组件

    1)通常,在项目的接口中经常会遇到一些相同的部分,当这个相同的不出频繁出现时,我们可以将其抽取出来,作为一个全局的变量供我们调用,定义全局变量我们可以通过定义【用户定义的变量】来实现;
    2)这里我们抽取 HTTP 请求的【服务器名称】和【端口号】来举例。
    3)添加【用户定义的变量】组件:右击【测试计划】—>【添加】—>【配置元件】—>【用户定义的变量】,如图所示,
    这里写图片描述
    4)【用户定义的变量】面板如下图所示,我们依次来介绍。
    这里写图片描述

    • ①取个见名知意的名字
    • ②点击【添加】,就会出现③中的输入框;点击【删除】,就会在③中删除一个变量。
    • ③【名称】:即参数名,变量名;【值】:参数值,变量值;【Description】:对变量的描述信息,可不填。
      这里写图片描述

    5)调用方式和之前一样,HTTP 请求的【服务器名称】和【端口号】的位置调用这两个变量:
    这里写图片描述
    6)查看执行结果,如下:
    这里写图片描述
          这里我们要注意的是:【用户定义的变量】是全局变量,即这里定义的变量的所有值只会初始化一次,无论后面创建了多少线程,值都不会变。也正是因为这个原因,为了不占用过多资源,建议不要创建太多这种类型的变量。

    4.【函数】组件

          在jmeter 中已经内置了一些【函数】供我们使用,给我们的测试工作提供了很大的便利。在jmeter中常用的【函数】主要有6个,我们通过其中的__counter()为例来介绍【函数】组件的使用方式,后面再介绍每个函数的功能。
    1)调出【函数】组件有三种方式,我们在使用时选择任意一种即可:

    • 方式一:菜单栏【选项】—>【函数助手对话框】

    这里写图片描述

    • 方式二:工具栏中倒数第二个类似于笔记本的按钮【函数助手对话框】
      这里写图片描述
    • 方式三:使用快捷键:
      • Windows 平台:Ctrl+shift+F1
      • Mac 平台:shift+command+F1
    1. 执行1)中的操作后,会出现下面的对话框:
      这里写图片描述
    • ①选择我们需要使用的函数__counter

    • ②设置函数调用时需要的参数:

      • 第一行:设置此计数器的作用范围:

        • TRUE:用户范围内有效,在本例中我们可以认为是单次循环内有效;
        • FALSE:全局范围内有效,即在多次循环范围内有效。
      • 第二行:这里要给函数的返回值取个名字,以便后面调用,这个是选填参数;

    • ③点击【生成】按钮,在输入框中就会出现完整的函数调用代码,我们直接复制到需要调用该函数的位置即可。
      这里写图片描述

    3) 为了直观体现出参数的作用,我们把线程组的【循环次数】设置为多次,这里就设置为3。

    • 当参数选择 TRUE 时:
      这里写图片描述
      这里写图片描述
      查看结果,我们可以发现,每次循环都是从1开始的。
    • 当参数选择 FALSE 时:
      这里写图片描述
      查看结果,我们可以发现,所有循环使用同一个计数器,新循环开始时,计数器不再重置为1,而是继上次循环结束时的结果递增。

    至此,函数的调用方式就介绍完了,下面就介绍一下jmeter中内置的6个常用函数:

    • __counter():计数器,上面介绍过了,就不再赘述。

    • __random():产生一个随机数。

      • 参数1:随机范围的低边界值,即随机范围内的最小值;
      • 参数2:随机范围的高边界值,即随机范围内的最大值;
      • 如下图中的设置所示,随机范围为[0,100] ,即两个边界值都可能取到。
        这里写图片描述
    • __time():返回当前时间,默认为距离1970年1月1日 0时0分0秒的毫秒值。

      • 参数1:设置返回的时间表现形式,
        • yyyy:表示年份
        • MM:表示月份
        • dd:表示日期
        • hh/HH:表示小时(hh:12小时制,HH:24小时制);
        • mm:表示分钟
        • ss:表示秒
          这里写图片描述
    • __CSVRead():读取外部 CSV 格式文件,类似于 CSV Data Set Config 组件,用来批量操作数据。

      • 参数1:CSV 文件路径,注意这里要填写 CSV 文件的绝对路径;

      • 参数2:要读取CSV 文件中哪一个字段的值,这里填写的是字段对应的 index 值,index 从0开始计数,即第一个字段对应的 index 为0 。
        这里写图片描述

      • 注意:此函数读取 CSV 文件要注意:

        • 此函数调用一次,只能读取一个字段的值,所以想要读出所有字段,需要多次调用此函数;
        • 将线程组的【线程数】设置为与 CSV 文件数据行数相同,可以读出当前字段对应的所有值;
      • 查看结果

        • 线程数设置为3,(与 CSV 文件数据行相同)
        • 只用一次 HTTP 请求,读取一个字段的值
          这里写图片描述
          这里写图片描述
          结果显示,我们只读取了一个字段对应的所有数据。
        • 下面我们再创建一个 HTTP 请求,多调用一次此函数,读取两个字段的值,注意第二各字段对应的 index 为1,不要忘记修改。
          这里写图片描述
          由结果看出,线程数为3,发送两次请求,调用两次此函数,成功的读取出了 CSV 文件中的两个字段对应三条数据的所有值。
    • __setProperty() 和 __property():用于线程组之间传参。

      • __setProperty() 在线程组1中将属性及属性值放到测试计划的共享空间中;
      • __property()在线程组2中从共享空间中获取属性。

    至此,Jmeter的参数化实现方式就介绍完了,希望以后看到的时候不会迷糊!哈哈~~

    展开全文
  • 对于以上例子,我们可以说类型形参T是泛型函数isInstanceOf的实化类型参数。 3、关于inline函数补充一点 我们对inline函数应该陌生,使用它最大一个好处就是函数调用的性能优化和提升,但是需要注意这里使用...

    Kotlin系列文章,欢迎查看:

    原创系列:

    翻译系列:

    实战系列:

    简述:
    今天我们开始接着原创系列文章,首先说下为什么不把这篇作为翻译篇呢?我看了下作者的原文,里面讲到的,这篇博客都会有所涉及。这篇文章将会带你全部弄懂Kotlin泛型中的reified实化类型参数,包括它的基本使用、源码原理、以及使用场景。有了上篇文章的介绍,相信大家对kotlin的reified实化类型参数有了一定认识和了解。那么这篇文章将会更加完整地梳理Kotlin的reified实化类型参数的原理和使用。废话不多说,直接来看一波章节导图:

    一、泛型类型擦除

    通过上篇文章我们知道了JVM中的泛型一般是通过类型擦除实现的,也就是说泛型类实例的类型实参在编译时被擦除,在运行时是不会被保留的。基于这样实现的做法是有历史原因的,最大的原因之一是为了兼容JDK1.5之前的版本,当然泛型类型擦除也是有好处的,在运行时丢弃了一些类型实参的信息,对于内存占用也会减少很多。正因为泛型类型擦除原因在业界Java的泛型又称伪泛型。因为编译后所有泛型的类型实参类型都会被替换Object类型或者泛型类型形参指定上界约束类的类型。例如:
    List<Float>、List<String>、List<Student>在JVM运行时Float、String、Student都被替换成Object类型,如果是泛型定义是List<T extends Student>那么运行时T被替换成Student类型,具体可以通过反射Erasure类可看出。

    虽然Kotlin没有和Java一样需要兼容旧版本的历史原因,但是由于Kotlin编译器编译后出来的class也是要运行在和Java相同的JVM上的,JVM的泛型一般都是通过泛型擦除,所以Kotlin始终还是迈不过泛型擦除的坎。但是Kotlin是一门有追求的语言不想再被C#那样喷Java说什么泛型集合连自己的类型实参都不知道,所以Kotlin借助inline内联函数玩了个小魔法。

    二、泛型擦除会带来什么影响?

    泛型擦除会带来什么影响,这里以Kotlin举例,因为Java遇到的问题,Kotlin同样需要面对。来看个例子

    fun main(args: Array<String>) {
        val list1: List<Int> = listOf(1,2,3,4)
        val list2: List<String> = listOf("a","b","c","d")
        println(list1)
        println(list2)
    }
    

    上面两个集合分别存储了Int类型的元素和String类型的元素,但是在编译后的class文件中的他们被替换成了List原生类型一起来看下反编译后的java代码

    @Metadata(
       mv = {1, 1, 11},
       bv = {1, 0, 2},
       k = 2,
       d1 = {"\u0000\u0014\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\u0010\u000e\n\u0002\b\u0002\u001a\u0019\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00040\u0003¢\u0006\u0002\u0010\u0005¨\u0006\u0006"},
       d2 = {"main", "", "args", "", "", "([Ljava/lang/String;)V", "Lambda_main"}
    )
    public final class GenericKtKt {
       public static final void main(@NotNull String[] args) {
          Intrinsics.checkParameterIsNotNull(args, "args");
          List list1 = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4});//List原生类型
          List list2 = CollectionsKt.listOf(new String[]{"a", "b", "c", "d"});//List原生类型
          System.out.println(list1);
          System.out.println(list2);
       }
    }
    

    我们看到编译后listOf函数接收的是Object类型,不再是具体的String和Int类型了。

    1、类型检查问题:

    Kotlin中的is类型检查,一般情况不能检测类型实参中的类型(注意是一般情况,后面特殊情况会细讲),类似下面。

    if(value is List<String>){...}//一般情况下这样的代码不会被编译通过
    

    分析: 尽管我们在运行时能够确定value是一个List集合,但是却无法获得该集合中存储的是哪种类型的数据元素,这就是因为泛型类的类型实参类型被擦除,被Object类型代替或上界形参约束类型代替。但是如何去正确检查value是否List呢?请看以下解决办法

    Java中的解决办法: 针对上述的问题,Java有个很直接解决方式,那就是使用List原生类型。

    if(value is List){...}
    

    Kotlin中的解决办法:
    我们都知道Kotlin不支持类似Java的原生类型,所有的泛型类都需要显示指定类型实参的类型,对于上述问题,kotlin中可以借助星投影List<*>(关于星投影后续会详细讲解)来解决,目前你暂且认为它是拥有未知类型实参的泛型类型,它的作用类似Java中的List<?>通配符。

    if(value is List<*>){...}
    

    特殊情况: 我们说is检查一般不能检测类型实参,但是有种特殊情况那就是Kotlin的编译器智能推导(不得不佩服Kotlin编译器的智能)

    fun printNumberList(collection: Collection<String>) {
        if(collection is List<String>){...} //在这里这样写法是合法的。
    }
    

    分析: Kotlin编译器能够根据当前作用域上下文智能推导出类型实参的类型,因为collection函数参数的泛型类的类型实参就是String,所以上述例子的类型实参只能是String,如果写成其他的类型还会报错呢。

    2、类型转换问题:

    在Kotlin中我们使用as或者as?来进行类型转换,注意在使用as转换时,仍然可以使用一般的泛型类型。只有该泛型类的基础类型是正确的即使是类型实参错误也能正常编译通过,但是会抛出一个警告。一起来看个例子

    package com.mikyou.kotlin.generic
    
    
    fun main(args: Array<String>) {
        printNumberList(listOf(1, 2, 3, 4, 5))//传入List<Int>类型的数据
    }
    
    fun printNumberList(collection: Collection<*>) {
        val numberList = collection as List<Int>//强转成List<Int>
        println(numberList)
    }
    

    运行输出

    package com.mikyou.kotlin.generic
    
    
    fun main(args: Array<String>) {
        printNumberList(listOf("a", "b", "c", "d"))//传入List<String>类型的数据
    }
    
    fun printNumberList(collection: Collection<*>) {
        val numberList = collection as List<Int>
        //这里强转成List<Int>,并不会报错,输出正常,
        //但是需要注意不能默认把类型实参当做Int来操作,因为擦除无法确定当前类型实参,否则有可能出现运行时异常
        println(numberList)
    }
    

    运行输出

    如果我们把调用地方改成setOf(1,2,3,4,5)

    fun main(args: Array<String>) {
        printNumberList(setOf(1, 2, 3, 4, 5))
    }
    
    fun printNumberList(collection: Collection<*>) {
        val numberList = collection as List<Int>
        println(numberList)
    }
    

    运行输出

    分析: 仔细想下,得到这样的结果也很正常,我们知道泛型的类型实参虽然在编译期被擦除,泛型类的基础类型不受其影响。虽然不知道List集合存储的具体元素类型,但是肯定能知道这是个List类型集合不是Set类型的集合,所以后者肯定会抛异常。至于前者因为在运行时无法确定类型实参,但是可以确定基础类型。所以只要基础类型匹配,而类型实参无法确定有可能匹配有可能不匹配,Kotlin编译采用抛出一个警告的处理。

    注意: 不建议这样的写法容易存在安全隐患,由于编译器只给了个警告,并没有卡死后路。一旦后面默认把它当做强转的类型实参来操作,而调用方传入的是基础类型匹配而类型实参不匹配就会出问题。

    package com.mikyou.kotlin.generic
    
    
    fun main(args: Array<String>) {
        printNumberList(listOf("a", "b", "c", "d"))
    }
    
    fun printNumberList(collection: Collection<*>) {
        val numberList = collection as List<Int>
        println(numberList.sum())
    }
    

    运行输出

    三、什么是reified实化类型参数函数?

    通过以上我们知道Kotlin和Java同样存在泛型类型擦除的问题,但是Kotlin作为一门现代编程语言,他知道Java擦除所带来的问题,所以开了一扇后门,就是通过inline函数保证使得泛型类的类型实参在运行时能够保留,这样的操作Kotlin中把它称为实化,对应需要使用reified关键字。

    1、满足实化类型参数函数的必要条件

    • 必须是inline内联函数,使用inline关键字修饰
    • 泛型类定义泛型形参时必须使用reified关键字修饰

    2、带实化类型参数的函数基本定义

    inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T 
    

    对于以上例子,我们可以说类型形参T是泛型函数isInstanceOf的实化类型参数。

    3、关于inline函数补充一点

    我们对inline函数应该不陌生,使用它最大一个好处就是函数调用的性能优化和提升,但是需要注意这里使用inline函数并不是因为性能的问题,而是另外一个好处它能是泛型函数类型实参进行实化,在运行时能拿到类型实参的信息。至于它是怎么实化的可以接着往下看

    四、实化类型参数函数的背后原理以及反编译分析

    我们知道类型实化参数实际上就是Kotlin变得的一个语法魔术,那么现在是时候揭开魔术神秘的面纱了。说实在的这个魔术能实现关键得益于内联函数,没有内联函数那么这个魔术就失效了。

    1、原理描述

    我们都知道内联函数的原理,编译器把实现内联函数的字节码动态插入到每次的调用点。那么实化的原理正是基于这个机制,每次调用带实化类型参数的函数时,编译器都知道此次调用中作为泛型类型实参的具体类型。所以编译器只要在每次调用时生成对应不同类型实参调用的字节码插入到调用点即可。 总之一句话很简单,就是带实化参数的函数每次调用都生成不同类型实参的字节码,动态插入到调用点。由于生成的字节码的类型实参引用了具体的类型,而不是类型参数所以不会存在擦除问题。

    2、reified的例子

    带实化类型参数的函数被广泛应用于Kotlin开发,特别是在一些Kotlin的官方库中,下面就用Anko库(简化Android的开发kotlin官方库)中一个精简版的startActivity函数

    inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any?>) =
            AnkoInternals.internalStartActivity(this, T::class.java, params)
    

    通过以上例子可看出定义了一个实化类型参数T,并且它有类型形参上界约束Activity,它可以直接将实化类型参数T当做普通类型使用

    3、代码反编译分析

    为了好反编译分析单独把库中的那个函数拷出来取了startActivityKt名字便于分析。

    class SplashActivity : BizActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.biz_app_activity_welcome)
            startActivityKt<AccountActivity>()//只需这样就直接启动了AccountActivity了,指明了类型形参上界约束Activity
        }
    }
    
    inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any?>) =
            AnkoInternals.internalStartActivity(this, T::class.java, params)
    

    编译后关键代码

    //函数定义反编译
     private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
          Intrinsics.reifiedOperationMarker(4, "T");
          AnkoInternals.internalStartActivity($receiver, Activity.class, params);//注意点一: 由于泛型擦除的影响,编译后原来传入类型实参AccountActivity被它形参上界约束Activity替换了,所以这里证明了我们之前的分析。
       }
    //函数调用点反编译
    protected void onCreate(@Nullable Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          this.setContentView(2131361821);
          Pair[] params$iv = new Pair[0];
          AnkoInternals.internalStartActivity(this, AccountActivity.class, params$iv);
          //注意点二: 可以看到这里函数调用并不是简单函数调用,而是根据此次调用明确的类型实参AccountActivity.class替换定义处的Activity.class,然后生成新的字节码插入到调用点。
    }
    

    让我们稍微在函数加点输出就会更加清晰

    class SplashActivity : BizActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.biz_app_activity_welcome)
            startActivityKt<AccountActivity>()
        }
    }
    
    inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any?>) {
        println("call before")
        AnkoInternals.internalStartActivity(this, T::class.java, params)
        println("call after")
    }
    

    反编译后

    private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
          String var3 = "call before";
          System.out.println(var3);
          Intrinsics.reifiedOperationMarker(4, "T");
          AnkoInternals.internalStartActivity($receiver, Activity.class, params);
          var3 = "call after";
          System.out.println(var3);
       }
    
       protected void onCreate(@Nullable Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          this.setContentView(2131361821);
          Pair[] params$iv = new Pair[0];
          String var4 = "call before";
          System.out.println(var4);
          AnkoInternals.internalStartActivity(this, AccountActivity.class, params$iv);//替换成确切的类型实参AccountActivity.class
          var4 = "call after";
          System.out.println(var4);
       }
       
    

    五、实化类型参数函数的使用限制

    这里说的使用限制主要有两点:

    1、Java调用Kotlin中的实化类型参数函数限制

    明确回答Kotlin中的实化类型参数函数不能在Java中的调用,我们可以简单的分析下,首先Kotlin的实化类型参数函数主要得益于inline函数的内联功能,但是Java可以调用普通的内联函数但是失去了内联功能,失去内联功能也就意味实化操作也就化为泡影。故重申一次Kotlin中的实化类型参数函数不能在Java中的调用

    2、Kotlin实化类型参数函数的使用限制

    • 不能使用非实化类型形参作为类型实参调用带实化类型参数的函数
    • 不能使用实化类型参数创建该类型参数的实例对象
    • 不能调用实化类型参数的伴生对象方法
    • reified关键字只能标记实化类型参数的内联函数,不能作用与类和属性。

    欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不定期翻译一篇Kotlin国外技术文章。如果你也喜欢Kotlin,欢迎加入我们~~~

    Kotlin系列文章,欢迎查看:

    Kotlin邂逅设计模式系列:

    数据结构与算法系列:

    翻译系列:

    原创系列:

    Effective Kotlin翻译系列

    实战系列:

    展开全文
  • LoadRunner的参数化保证重复

    千次阅读 2016-02-02 13:56:49
    当我需要将一个id进行参数化,但是又想自己创建数据,或者无论运行多少次都能保持数据重复的话,在参数化可以选择Date/time类型作为id。 比如我想创建一个文件,但是文件的名称是允许重复的,我就可以将...
  • MyBatis面试题(2020最新版)

    万次阅读 多人点赞 2019-09-24 16:40:33
    整理好的MyBatis面试题库,史上最全的MyBatis面试题,MyBatis面试宝典,特此分享给大家 MyBatis 介绍 MyBatis 是一款优秀的...MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plai...
  • python sql语句参数化接口

    千次阅读 2018-06-12 18:22:52
    由于工作需要,今天写了一个Python小脚本,其中需要连接MySQL数据库,在执行sql命令时需要传递参数,结果出问题了。在网上查了一下,发现有以下几种方式传递参数:一.直接把sql查询语句完整写入字符串1 try: 2 ...
  • Kotlin泛型类型参数

    千次阅读 2018-03-13 13:57:21
    Kotlin泛型类型参数 泛型允许你...类型参数可以准确清晰地进行描述,就像这样“这个变量保存了字符串列表”,而不是“这个变量保存了一个列表”。Kotlin说明“字符串列表”的语法和Java看起来一样:List&lt;St...
  • jmeter连接数据库-取数据并参数化

    千次阅读 2019-07-02 18:16:59
    只连过MySQL的数据库,知道其它数据库怎么操作,方法应该差不多 在线程组下面加JDBC Connection Configuration Variable Name:这里写个名,后面要用到 Database URL输入格式:jdbc:mysql://数据库地址/库名 ...
  • 入门学习Linux常用必会60个命令实例详解doc/txt

    千次下载 热门讨论 2011-06-09 00:08:45
    前面介绍了Linux下有多种Shell,一般缺省的是Bash,如果想更换Shell类型可以使用chsh命令。先输入账户密码,然后输入新Shell类型,如果操作正确系统会显示“Shell change”。其界面一般如下: Changing fihanging ...
  • 泛型其实指得就是参数化类型,使得代码可以适应多种类型。像容器,List< T >,大量使用了泛型 它的主要目的之一就是用来指定容器要持有什么类型的对象 泛型只能只能代表引用类型,能是原始类型,原始类型...
  • C++基础——非类型模板参数

    万次阅读 多人点赞 2015-11-04 10:17:08
    类型模板参看,顾名思义,模板参数不限定于类型,普通值也可作为模板参数。 1. 非类型类模板参数 2. 非类型函数模板参数 3. 非类型模板参数的限制 (1). 如何以非常量类型(double或class-type)作为类型模板...
  • java8新特性---函数参数化(::)

    万次阅读 2018-10-11 09:58:22
    将函数作为参数化,并进行传递 1、定义函数接口 @FunctionalInterface public interface ConvertPredict&lt;T,V&gt; { /** * 转换函数 * @param t * @param v */ void convert(T t,V v); } 2、...
  • C语言

    万次阅读 多人点赞 2019-03-28 11:39:01
    数组作为函数参数 函数指针 返回值为指针的函数 void指针与NULL指针 动态分配内存 C/C++字符串 结构体 结构体数组 结构体指针 共用体 枚举 c语言 内存四区 c语言 预处理k c语言 ...
  • 天线基础与HFSS天线设计流程

    万次阅读 多人点赞 2019-04-28 15:10:10
    1.2 天线的性能参数 1. 方向图 2. 辐射强度 3.方向性系数 4. 效率 5. 增益 6. 输入阻抗 7. 天线的极 HFSS天线设计流程 2.1 HFSS天线设计流程概述 1.设置求解类型 2.创建天线的结构模型 3.设置边界...
  • 而变化型注解(Variance Annotation)定义了参数化类型的继承关系,比如Set[String]是Set[AnyRef]的子类型。 这些语法可以让我们实现信息隐藏技术,同时它们也是编写库程序的基础。类型参数化这里以水果盒的代码...
  • JAVA上百实例源码以及开源项目

    千次下载 热门讨论 2016-01-03 17:37:40
     Java波浪文字,一个利用Java处理字符的实例,可以设置运动方向参数,显示文本的字符数组,高速文本颜色,显示字体的 FontMetrics对象,得到Graphics实例,得到Image实例,填充颜色数组数据,初始颜色数组。...
  • 30.scala编程思想笔记——参数化类型

    万次阅读 2016-01-01 20:35:37
    30.scala编程思想笔记——参数化类型 欢迎转载,转载请标明出处: 源码下载连接请见第一篇笔记。 尽量让Scala完成推断类型是一个不错的主意,这样会使代码变得更整洁且更易于阅读。有时候无法识别出是什么类型,...
  • 1、MultipartFile作为参数,通过分布式调用接口直接上传,因为是final 无法实现序列; 2、通过inputStream 作为参数能传参; InputStream inputStream = file.getInputStream(); 在接口的实现类中,...
  • 使用对象数组作为参数,只是将对象作为一个数组元素,其引用方法与基本数据类型的数组完全相同。但针对对象数组中的单个元素来讲,它又具有对象有属性和方法。 import java.util.Scanner; //学生类class Student{ ...
  • 数组存储的数据类型:创建的数组容器可以存储什么数据类型。 【】:表示数组。 数组名字:为定义的数组起个变量名,满足标识符规范,可以使用名字操作数组。 new:关键字。 数组存储的数据类型:创建的数组容器...
  • Java能直接用函数作为形参,只能传递对象的引用,所以在需要回调函数时,往往传递的是接口的匿名实现。 二、高阶函数 示例 高阶函数:以另一个函数作为参数,或者返回值是函数 称为 高阶函数 需要补充一些...
  • 参数化测试 实例 需要 使用参数 源的参数(Sources of Arguments) @ValueSource 实例 @EnumSource @MethodSource 自动搜索 原始类型的流 多个参数 外部的静态工厂方法 @CsvSource @CsvFileSource @...
  • 模板非类型形参的详细阐述

    千次阅读 2016-03-02 15:34:58
    如果想进一步理解非类型形参以及模板内容可以阅读C++template这本书,在4.1节,8.3.3节,13.2节都有相关解释。 这里要强调一点,我们对于非类型形参的限定要分两个方面看 1.对模板形参的限定,即template里面的参数 ...
  • java8类型推断及它的默认类型推断行为
  • 当我们使用基于接口或者抽象类创建的DataContractSerializer去序列一个实现了该接口或者继承该抽象类的实例的时候,往往会因为对对象的真实类型无法识别造成能正常地序列。  现在,我们定义两个带数据
  • C++ 引用与引用作为函数的参数

    千次阅读 多人点赞 2019-05-12 13:33:39
    (有一个例外,引用作为函数参数时,需要初始)  (2)在声明一个引用后,能再使之作为另一变量的引用。  (3)能建立引用数组。 引用的作用: C++加入了在C语言的基础加入了引用机制,那么引用...
  • python中将字典作为参数传入函数

    千次阅读 2020-04-26 18:27:44
    (主要作为自己学习记录,如有不足,希望指正) 定义函数是需要加入参数 **kwargs, 实例1如下: # -*- coding:utf-8 -*- def name_age(**kwargs): file = {} for key, value in kwargs.items(): file[key] = ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,188,060
精华内容 475,224
关键字:

参数化类型不可以作为