美文网首页
科普|泛型(出现历史、定义、用法、原理)

科普|泛型(出现历史、定义、用法、原理)

作者: 蚍蜉一生 | 来源:发表于2024-07-01 09:58 被阅读0次

    一、泛型是什么 ,为啥会有泛型

        在泛型出现前,Java等强类型语言中方法的参数、返回值、类的成员变量、局部变量都必须是一个特定数据类型。比如:要找出Integer、Double 数组和 Float 数组中的最大值,如果没有泛型,就需要为每种类型编写单独的方法:

    // 找出 Integer 数组中的最大值
        public static Integer findMax(Integer[] array) {
            Integer max = array[0];
            for (Float element : array) {
                if (element > max) {
                    max = element;
                }
            }
            return max;
        }
    
    
     // 找出 Float 数组中的最大值
        public static Float findMax(Float[] array) {
            Float max = array[0];
            for (Float element : array) {
                if (element > max) {
                    max = element;
                }
            }
            return max;
        }
    
      // 找出 Double 数组中的最大值
        public static Double findMax(Double[] array) {
            Double max = array[0];
            for (Double element : array) {
                if (element > max) {
                    max = element;
                }
            }
            return max;
        }
    
    .......
    

    观察这些方法发现,除了方法的入参、返回值、还有关联的局部变量不一样,每个方法的运算步骤可以说是一毛一样。那如何在数据类型不一样的情况下实现思想和步骤复用呢?我们很容易想到向上转型,但向上转型会丢失类型限制,出现诸如返回值不满足类型要求,类型编译时检查失效从而导致的运行时类型转换错误等问题。
        这种情况下,泛型出现了,它既实现了方法的复用,又能对数据类型做一致性限制,如下所示:

       // 定义一个泛型方法来找出数组中的最大值
       //  <T extends Comparable<T>> 是泛型声明,声明该泛型的类型必须是Comparable<T>的子类 泛型类型为T
       //  泛型类型声明为T之后,后面使用的入参类型T[],局部变量类型、返回值类型都使用了T类型,就是说编译时候会检查这类型是否一致
        public static <T extends Comparable<T>> T findMax(T[] array) {
            T max = array[0];
            for (T element : array) {
                if (element.compareTo(max) > 0) {
                    max = element;
                }
            }
            return max;
        }
    
    类型安全限制

    如上图,case1 findMax入参是Integer[],即T = Integer,它的返回值是Integer,所以赋值给i没有错;case2 中findMax入参是Float[] 即T = Float,它的返回值就是Float,那么直接赋值给Integer,就会报错,就是通过泛型这种机制实现了数据类型一致性的限制,避免了运行时类型转换出错。
        在面向对象语言中,所有方法都在类中,所以泛型不仅能够解决方法的复用,也能够解决类的成员变量、局部变量等的复用(如何用下一个章节介绍),总结下泛型的作用有两点:

    1. 类和方法复用:可以使用调用者传入类型;

    2. 类型限制: 一致性、类型上下界限制(子类、父类限制);

    二、泛型如何使用

    泛型使用遵守先声明后使用的步骤,常见的有以下几种:

    2.1 泛型类

    public class Box<T> {  // <T>是泛型声明 跟在类名后面
        private T content;     // 泛型使用,这里说名了 content是T类型的
    
        public void setContent(T content) {
            this.content = content;
        }
    
        public T getContent() {     // 泛型使用 这里说明 getContent 返回类型是T
            return content;
        }
    
        public static void main(String[] args) {
            Box<String> stringBox = new Box<>();   //  这里Box<> = Box<String> jdk7之后可以省略,编译器根据前面自动推导,通过构造函数将T类型赋值为String类型
            stringBox.setContent("Hello");         
            System.out.println("String content: " + stringBox.getContent()); // 输出:Hello
    
            Box<Integer> integerBox = new Box<>();
            integerBox.setContent(123);
            System.out.println("Integer content: " + integerBox.getContent()); // 输出:123
        }
    }
    

    常见的泛型类有Map、Set、List以及他们的子类。

    2.2 泛型方法

    public class GenericMethodExample {
    
        // 泛型方法,<T> 是泛型声明,需要放在返回类型之前声明 
        // T[] 是泛型使用,表明入参是T类型的数组
        public static <T> T getFirst(T[] array) {
            if (array == null || array.length == 0) {
                return null;
            }
            return array[0];
        }
    
        public static void main(String[] args) {
            String[] stringArray = {"Hello", "World"};
            Integer[] intArray = {1, 2, 3};
    
           
            // <String> 在方法前面显式指出了返回类型
            String firstString = GenericMethodExample.<String>getFirst(stringArray); 
            // intArray 和firstInt类型 隐式推断类型参数
            Integer firstInt = getFirst(intArray); 
    
            System.out.println("First String: " + firstString); // 输出:First String: Hello
            System.out.println("First Integer: " + firstInt); // 输出:First Integer: 1
        }
    }
    

    接下来是两个参数的泛型方法

    public class DualGenericExample {
    
    
        @RequiresApi(api = Build.VERSION_CODES.N)
        // <T, U> 是泛型声明 U是返回值类型  T是一个输入参数类型
        // Function<T, U> converter 然后 T U 又传递到一个泛型接口中 当作它的两个参数的类型
        public static <T, U> U convertAndPrint(T input, Function<T, U> converter) {
            U result = converter.apply(input);
            System.out.println("Converted Result: " + result);
            return result;
        }
    
        @RequiresApi(api = Build.VERSION_CODES.N)
        public static void main(String[] args) {
            // 示例1:将整数转换为字符串并打印
            Integer num = 123;
            String numStr = convertAndPrint(num, new Function<Integer, String>() {
                @Override
                public String apply(Integer integer) {
                    return String.valueOf(integer);
                }
            });
            // 输出:Converted Result: 123
    
            // 示例2:将字符串转换为其长度并打印
            String str = "Hello";
            Integer length = convertAndPrint(str, new Function<String, Integer>() {
                @Override
                public Integer apply(String s) {
                    return s.length();
                }
            });
            // 输出:Converted Result: 5
        }
    }
    

    2.3 泛型接口

    泛型接口允许我们在接口定义中使用类型参数,从而使接口能够处理不同类型的数据。

    // 类似于泛型类
    public interface Comparable<T> {
        int compareTo(T o);
    }
    
    public class GenericInterfaceExample implements Comparable<GenericInterfaceExample> {
        private int value;
    
        public GenericInterfaceExample(int value) {
            this.value = value;
        }
    
        @Override
        public int compareTo(GenericInterfaceExample other) {
            return Integer.compare(this.value, other.value);
        }
    
        public static void main(String[] args) {
            GenericInterfaceExample obj1 = new GenericInterfaceExample(10);
            GenericInterfaceExample obj2 = new GenericInterfaceExample(20);
            System.out.println(obj1.compareTo(obj2)); // 输出:-1(因为 10 < 20)
        }
    }
    
    

    2.4 泛型通配符

        在未引入泛型通配符时,泛型的限制是类型一致性限制(必须都是声明的类型T);引入通配符之后限制变松了,有三种:无界通配符<?> (无限制)上界通配符<? extends T >(必须是T类型及其子类型)和下界通配符<? super T>(必须是T类型及父类型),分别举例如下:

    import java.util.List;
    // 无限制的通配符,不管什么类型都可以打印,一般用于只需要读取数据,而不需要写入数据
    public class UnboundedWildcardExample {
        public static void printList(List<?> list) {
            for (Object obj : list) {
                System.out.println(obj);
            }
        }
    
        public static void main(String[] args) {
            List<String> stringList = List.of("Hello", "World");
            List<Integer> intList = List.of(1, 2, 3);
    
            printList(stringList); // 可以打印 List<String>
            printList(intList);    // 可以打印 List<Integer>
        }
    }
    
    import java.util.List;
    //上界通配符 限制父类,比如是Number子类的 才能使用,适用于需要读取某种类型及其子类型的数据的情况。
    public class UpperBoundedWildcardExample {
        public static void printNumbers(List<? extends Number> list) {
            for (Number num : list) {
                System.out.println(num);
            }
        }
    
        public static void main(String[] args) {
            List<Integer> intList = List.of(1, 2, 3);
            List<Double> doubleList = List.of(1.1, 2.2, 3.3);
    
            printNumbers(intList);   // 可以打印 List<Integer>
            printNumbers(doubleList); // 可以打印 List<Double>
        }
    }
    
    
    import java.util.List;
    import java.util.ArrayList;
    // 下界通配符 限制子类,必须是Integer的父类才能使用该方法
    // 适合写入某种类型及子类型的数据的情况,注意理解下,因为方法是限制的子类型
    // 下届通配符 是限制下界,比如本例子中,是限制是必须是Integer父类才能使用该方法,这个父类是
    // 使用者给的,比如Number,所以上面说 适合是写入某种类型 Number以及子类Integer写入使用。
    public class LowerBoundedWildcardExample {
        public static void addNumbers(List<? super Integer> list) {
            list.add(1);
            list.add(2);
            list.add(3);
        }
    
        public static void main(String[] args) {
            List<Number> numberList = new ArrayList<>();
            addNumbers(numberList); // 可以向 List<Number> 添加 Integer
    
            List<Object> objectList = new ArrayList<>();
            addNumbers(objectList); // 可以向 List<Object> 添加 Integer
        }
    }
    
    
    // 类型要求一致性的场景,
    public static <T> void addElement(List<T> list, T element) {
        list.add(element);
    }
    
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        addElement(stringList, "Hello"); // 可以添加元素
    
        List<Integer> intList = new ArrayList<>();
        addElement(intList, 1); // 可以添加元素
    }
    
    // 类型要求一致的场景,通配符? 不能使用
    public static void addElement(List<?> list, Object element) {
         list.add(element); // 编译错误,无法添加元素
    }
    
    

    总结下:泛型通配符就是从原来必须等于T 或者其他类型的限制,放松为无限制或者限制参数的父类和子类,使用更加灵活,类型安全降低。

    三、泛型实现原理简介

    回顾前文,泛型有两个作用:

    • 类和方法复用:类的成员变量、方法参数使用泛型定义后,调用者可以传入类型,从而使用;

    • 类型限制:未使用通配符的定义的泛型方法和类,所有使用地方传入的类型必须一致;使用通配符的定义的泛型方法和类,所有使用方不做类型限制,或者有类型上下界限制(子类、父类限制);

        类型擦除(泛型擦除)+强制类型转换 是泛型实现的主要原理,具体来说就是在编译时****,Java 编译器****会****将****泛型类型参数替换为它们的非泛型上界,如果没有指定上界,则替换为 Object,并且使用强制类型转换保证类型一致,这样就实现了在方法和类的复用(可以使用各种类型)和运行时候类型一致性。`
        除此之外编译器会使用泛型信息(定义、入参、返回值等)进行类型检查类型推断,以确保代码在编译时候是类型安全的。
        所以我们可以说泛型是编译时特性,虚拟机对泛型一无所知。

    四、什么时候使用泛型?

        当构建通用工具、通用算法、通用数据结构,就是需要复用的时候请考虑泛型,什么时候需要复用,就是当考虑通过规模化降低成本的时候。

    相关文章

      网友评论

          本文标题:科普|泛型(出现历史、定义、用法、原理)

          本文链接:https://www.haomeiwen.com/subject/yvipcjtx.html