泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。 一句话解释什么是泛型?泛型是相关语言特性的集合,它允许 引入泛型之前泛型在Java集合框架中被广泛使用,我们不使用泛型,那么代码将会是这样: List doubleList = new LinkedList(); doubleList.add(3.5D); Double d = (Double) doubleList.iterator().next(); //类型强制转换 doubleList中存储一个Double类型的值, 但是List并不能阻止我们往里面再添加一个String类型 比如:doubleList.add (“ Hello world ”); 最后一行的(Double)强制转换操作符将导致在遇到非 Double 对象时抛出 ClassCastException 引入泛型之后因为直到运行时才检测到类型安全性的缺失,所以开发人员可能不会意识到这个问题,将其留给客户机(而不是编译器)来发现。泛型允许开发人员将List标记为只包含 Double 对象,从而帮助编译器提醒开发人员在列表中存储非 Double 类型的对象的问题,在编译和开发期间,就把问题解决掉 我们可以这样改造上面的代码: List<Double> doubleList = new LinkedList<Double>(); doubleList.add(3.5D); Double d = doubleList.iterator().next(); 这时 我们再添加String类型的参数 会提示需要的类型不符合需求. 深入探索泛型类泛型的概念泛型是通过
一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。 泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。 泛型类型遵循语法泛型类型遵循以下语法: class identifier<formalTypeParameterList>{ } interface identifier<formalTypeParameterList>{ } interface Map<K,V> {//多个用逗号分隔 } 类型参数命名原则Java 编程约定要求类型参数名称为单个大写字母,例如 E 表示元素,K 表示键,V 表示值,T 表示类型。避免使用像A,B,C这样没有意义的名称。 List < E > 表示一个元素列表,但是 List < B > 的意思是什么呢? 实际类型参数 替换 类型参数泛型的 JAVA支持的实际类型的参数有哪些
class Container<E> { Set<E> elements; //E传给E }
例: List < Student > ,
例: Set < List < Shape > >,
例: Map < String, String[] >,
例: Class < ? > , ? 传给T 声明和使用泛型泛型的声明涉及到指定形式类型参数列表,并在整个实现过程中访问这些类型参数。使用泛型时需要在实例化泛型时将实际类型参数传递给类型参数 定义泛型的例子在本例子中,我们实现一个简易的容器Container,该容器类型存储相应参数类型的对象,使其能够存储各种类型 class Container<E> //也可以使用实际类型的参数 { private E[] elements; private int index; Container(int size) { elements = (E[]) new Object[size]; //本例中我们传入的是String,将Object[]转化为String[]返回 index = 0; } void add(E element) { elements[index++] = element; } E get(int index) { return elements[index]; } int size() { return index; } } public class GenDemo { public static void main(String[] args) { Container<String> con = new Container<String>(5);//使用String传给E,指定E为String类型的 con.add("North"); con.add("South"); con.add("East"); con.add("West"); for (int i = 0; i < con.size(); i++) System.out.println(con.get(i)); } } 指定类型参数的泛型Container < E > 中的 E 为无界类型参数,通俗的讲就是什么类型都可以,可以将任何实际的类型参数传递给 E 通过指定上限来限制传入的类但是有时你想限制类型,比如你想 < E > 只接受 Employee 及其子类 class Employees<E extends Employee> 此时传入的E 必须为 Employee子类, new Employees< String > 是无效的. 指定多个类型限制当然我们还可以为一个类指定多个类型 使用&分隔 : abstract class Employee { private BigDecimal hourlySalary; private String name; Employee(String name, BigDecimal hourlySalary) { this.name = name; this.hourlySalary = hourlySalary; } public BigDecimal getHourlySalary() { return hourlySalary; } public String getName() { return name; } public String toString() { return name + ": " + hourlySalary.toString(); } } class Accountant extends Employee implements Comparable<Accountant> /* Comparable < Accountant > 表明Accountant可以按照自然顺序进行比较 Comparable 接口声明为泛型类型,只有一个名为 t 的类型参数。 这个接口提供了一个 int compareTo (t o)方法,该方法将当前对象与参数(类型为 t)进行比较, 当该对象小于、等于或大于指定对象时返回负整数、零或正整数。 */ { Accountant(String name, BigDecimal hourlySalary) { super(name, hourlySalary); } public int compareTo(Accountant acct) { return getHourlySalary().compareTo(acct.getHourlySalary()); } } class SortedEmployees<E extends Employee & Comparable<E>> //第一个必须为class 之后的必须为interface { private E[] employees; private int index; @SuppressWarnings("unchecked") SortedEmployees(int size) { employees = (E[]) new Employee[size]; int index = 0; } void add(E emp) { employees[index++] = emp; Arrays.sort(employees, 0, index); } E get(int index) { return employees[index]; } int size() { return index; } } public class GenDemo { public static void main(String[] args) { SortedEmployees<Accountant> se = new SortedEmployees<Accountant>(10); se.add(new Accountant("John Doe", new BigDecimal("35.40"))); se.add(new Accountant("George Smith", new BigDecimal("15.20"))); se.add(new Accountant("Jane Jones", new BigDecimal("25.60"))); for (int i = 0; i < se.size(); i++) System.out.println(se.get(i)); } } 下界和泛型参数假设你想要打印出一个对象列表 class Scratch_12{ public static void main(String[] args) { { List<String> directions = new ArrayList(); directions.add("north"); directions.add("south"); directions.add("east"); directions.add("west"); printList(directions); List<Integer> grades = new ArrayList(); grades.add(new Integer(98)); grades.add(new Integer(63)); grades.add(new Integer(87)); printList(grades); } } static void printList(List<Object> list) { Iterator<Object> iter = list.iterator(); while (iter.hasNext()) System.out.println(iter.next()); } } 这个例子看似是合乎逻辑的,我们想通过将 List < object > 类型的对象传递给 printList ()方法,防止类型安全的这种冲突。然而,这样做并不是很有用。实际上编译器已经报出错误了,它告诉我们不能将字符串列表转换为对象列表 为什么会报这个错呢? 这和泛型的基本规则有关:
根据这个规则,尽管 String 和 Integer 是 java.lang.Object 的子类,但是List < string > 和 List < integer > 是 List < Object > 的子类就不对了. 为什么我们有这个规则?因为泛型的设计是为了在编译时捕获类型安全违规行为。如果没有泛型,我们可能会发生线上事故,因为程序抛出了 ClassCastException 并崩溃了! 作为演示,我们假设 List < string > 是 List < object > 的子类型。如果这是真的,你可能会得到以下代码: List<String> directions = new ArrayList<String>(); List<Object> objects = directions; objects.add(new Integer()); String s = objects.get(0); 将一个整数添加到对象列表中,这违反了类型安全。问题发生在最后一行,该行抛出 ClassCastException,因为无法将存储的整数强制转换为字符串。 使用通配符来解决问题class Scratch_13{ public static void main(String[] args) { List<String> directions = new ArrayList<String>(); directions.add("north"); directions.add("south"); directions.add("east"); directions.add("west"); printList(directions); List<Integer> grades = new ArrayList<Integer>(); grades.add(Integer.valueOf(98)); grades.add(Integer.valueOf(63)); grades.add(Integer.valueOf(87)); printList(grades); } static void printList (List < ? > list) { Iterator<?> iter = list.iterator(); while (iter.hasNext()) System.out.println(iter.next()); } } 我使用了一个通配符(?)在参数列表和 printList ()的方法体中,因为此符号代表任何类型,所以将 List < string > 和 List < integer > 传递给此方法是合法的 深入探索泛型方法假如你现在有一个业务逻辑需要你将一个List 复制到另外一个List,要传递任意类型的源和目标,需要使用通配符作为类型占位符 void copy(List<?> src, List<?> dest, Filter filter) { for (int i = 0; i < src.size(); i++) if (filter.accept(src.get(i))) dest.add(src.get(i)); } 这时编译器又又又报错了 < ? >意味着任何类型的对象都可以是列表的元素类型,并且源元素和目标元素类型可能是不兼容的 例:源列表是一个 Shape 的 List,而目标列表是一个 String 的 List,并且允许复制,那么在尝试检索目标列表的元素时将抛出 ClassCastException 指定类型上下界void copy(List<? extends String> src, List<? super String> dest, Filter filter) { for (int i = 0; i < src.size(); i++) if (filter.accept(src.get(i))) dest.add(src.get(i)); } 通过指定 extends 后跟类型名称,可以为通配符提供一个上限。类似地,可以通过指定 super 后跟类型名来为通配符提供一个下限。这些边界限制了可以作为实际类型参数传递的类型。 在这个例子中,因为 String 是 final,这意味着它不能被继承,所以只能传递 String 对象的源列表和 String 或 Object 对象的目标列表,这个问题只是解决了一部分,怎么办呢 使用泛型方法完全解决这个问题泛型方法的语法规范: <formalTypeParameterList> returnType method(param) 类型参数可以用作返回类型,也可以出现在参数列表中 此时我们重写代码解决这个问题: public class Demo { public static void main(String[] args) { List<Integer> grades = new ArrayList<Integer>(); Integer[] gradeValues = { Integer.valueOf(96), Integer.valueOf(95), Integer.valueOf(27), Integer.valueOf(100), Integer.valueOf(43), Integer.valueOf(68) }; for (int i = 0; i < gradeValues.length; i++){ grades.add(gradeValues[i]); } List<Integer> failedGrades = new ArrayList<Integer>(); copy(grades, failedGrades, grade -> grade <= 50);//函数式编程,使用lambda表达式实现Filter<T>此时T为Integer类型 for (int i = 0; i < failedGrades.size(); i++){ System.out.println(failedGrades.get(i)); } } static <T> void copy(List<T> src, List<T> dest, Filter<T> filter) { for (int i = 0; i < src.size(); i++) if (filter.accept(src.get(i))) dest.add(src.get(i)); } } interface Filter<T> { boolean accept(T o); } 此时我们为 src、 dest 和 filter 参数的类型都包含类型参数 T。这意味着在方法调用期间必须传递相同的实际类型参数,编译器自动通过调用来推断这个参数的类型是什么 泛型和类型推断Java 编译器包含类型推断算法,用于在实例化泛型类、调用类的泛型构造函数或调用泛型方法时识别实际的类型参数。 泛型类实例化在 Java SE 7之前,在实例化泛型类时,必须为变量的泛型类型和构造函数指定相同的实际类型参数。例子: Map<String, Set<String>> marbles = new HashMap<String, Set<String>>(); 此时,代码显得非常混乱,为了消除这种混乱,Java SE 7修改了类型推断算法,以便可以用空列表< >替换构造函数的实际类型参数,前提是编译器可以从实例化上下文中推断类型参数。示例: Map<String, Set<String>> marbles = new HashMap<>();//使用<>替换<String, Set<String>> 要在泛型类实例化期间利用类型推断,必须指定<>: Map<String, Set<String>> marbles = new HashMap(); 编译器生成一个“ unchecked conversion warning” ,因为 HashMap ()构造函数引用了 java.util。指定 HashMap 原始类型,而不是 HashMap<String, Set< String >>。 泛型构造函数泛型类和非泛型类都可以声明 public class Box<E> { public <T> Box(T t) { // ... } } 此声明使用形式类型参数 E 指定泛型类 Box < E > 。它还指定了一个具有形式类型参数 T 的泛型构造函数 那么在构造函数调用时是这样的: new Box<Marble>("Aggies"); 进一步利用菱形运算符来消除构造函数调用中的 Marble 实际类型参数,只要编译器能够从实例化上下文中推断出这个类型参数: new Box<>("Aggies"); 泛型方法调用我们现在已经知道了 编译器会通过类型推断算法识别出我们使用的类型 //copy是静态方法 我们可以使用class.methodName的方式调用它 Demo.<Integer>copy(grades, failedGrades, grade -> grade <= 50); 对于实例方法,语法几乎完全相同。 new Demo().<Integer>copy(grades, failedGrades, grade -> grade <= 50); 类型擦除
举例说明ArrayList< String > () 和 ArrayList< Integer > () 很容易被认为是不同的类型,但是下面的打印结果却是 true public class ErasedType { public static void main(String[] args) { Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); // output:true System.out.println(c1 == c2); } } System.out.println(Arrays.toString(c1.getTypeParameters())); // output:[E] System.out.println(Arrays.toString(c2.getTypeParameters())); // output:[E] 分别打印它们的参数类型,可以发现,无论指定的是 Integer 类型还是 String 类型,最后输出结果都仅是一个 用作参数占位符的标识符 [E] 而已. 这意味着,在使用泛型时,任何具体的类型信息,比如上例中的 Integer 或 String,在泛型内部都是无法获得的,也就是,被擦除了。唯一知道的,就只是正在使用着的对象。由于 ArrayList< String >() 和 ArrayList< Integer >() 都会被擦除成“原生态”(即 List) 如果指定了边界,例如< T extends Integer>,类型参数会被擦除为边界(Integer),如果未指定边界,例如,类型参数会被擦除为 Object 。 堆污染( heap pollution)在使用泛型时,可能会遇到堆污染,其中参数化类型的变量引用的对象不是该参数化类型(例如,如果原始类型与参数化类型混合)。在这种情况下,编译器报告“ 堆污染示例import java.util.Iterator; import java.util.Set; import java.util.TreeSet; public class Scratch_15 { public static void main(String[] args) { Set s = new TreeSet<Integer>(); Set<String> ss = s; // unchecked warning Unchecked assignment: 'java.util.Set' to 'java.util.Set<java.lang.String>' s.add(42); // unchecked warning Unchecked call to 'add(E)' as a member of raw type 'java.util.Set' Iterator<String> iter = ss.iterator(); while (iter.hasNext()) { String str = iter.next(); //throw ClassCastException System.out.println(str); } } } /* Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String at Scratch_15.main(scratch_15.java:17) */
@SafeVarargs的用法@SafeVarargs在JDK 7中引入,主要目的是处理可变长参数中的泛型,此注解告诉编译器:在可变长参数中的泛型是类型安全的。可变长参数是使用数组存储的,而数组和泛型不能很好的混合使用 import java.util.ArrayList; public class SafeVarargsTest { public static void main(String[] args) { ArrayList<Integer> a1 = new ArrayList<>(); a1.add(new Integer(1)); a1.add(2); showArgs(a1, 12); } //@SafeVarargs public static <T> void showArgs(T... array) { for (T arg : array) { System.out.println(arg.getClass().getName() + ":" + arg); } } } 如果使用IDE进行编译,需要修改编译参数,增加-Xlint:unchecked编译选项。 $ javac -Xlint:unchecked SafeVarargsTest.java SafeVarargsTest.java:18: 警告: [unchecked] 参数化 vararg 类型T的堆可能已受污染 public static < T> void showArgs(T… array) { ^ 其中, T是类型变量: 但是显然在这个示例中,可变参数的泛型是安全的,因此可以启用@SafeVarargs注解消除这个警告信息。 @SafeVarargs注解只能用在参数长度可变的方法或构造方法上,且方法必须声明为static或final,否则会出现编译错误。一个方法使用@SafeVarargs注解的前提是,开发人员必须确保这个方法的实现中对泛型类型参数的处理不会引发类型安全问题,否则可能导致运行时的类型转换异常。下面给出一个“堆污染”的实例 import java.util.Arrays; import java.util.List; public class UnsafeMethodTest { public static void main(String[] args) { List<String> list1 = Arrays.asList("one", "two"); List<String> list2 = Arrays.asList("three","four"); unsafeMethod(list1, list2); } @SafeVarargs //并不安全 static void unsafeMethod(List<String>... stringLists) { Object[] array = stringLists; List<Integer> tmpList = Arrays.asList(42, 56); array[0] = tmpList; // tmpList是一个List对象(类型已经擦除),赋值给Object类型的对象是允许的(向上塑型),能够编译通过 String s = stringLists[0].get(0); // 运行时抛出ClassCastException! } } 运行UnsafeMethodTest的结果如下: Exception in thread “main” java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 数组array和stringLists同时指向了参数数组,tmpList是一个包含两个Integer对象的list对象。 完 |
|