泛型

1年前 阅读 481 评论 0 赞 0

希望数据类型参数化的地方,就可以使用泛型。

泛型是什么?

用来规定一个类、接口或方法所能接受的数据的类型. 就像在声明方法时指定参数一样, 我们在声明一个类, 接口或方法时, 也可以指定其”类型参数”, 也就是泛型.

泛型的好处

  1. 提高安全性: 将运行期的错误转换到编译期. 如果我们在对一个对象所赋的值不符合其泛型的规定, 就会编译报错.
  2. 避免强转: 比如我们在使用List时, 如果我们不使用泛型, 当从List中取出元素时, 其类型会是默认的Object, 我们必须将其向下转型为String才能使用。比如:
  1. List l = new ArrayList();
  2. l.add("abc");
  3. String s = (String) l.get(0);

    而使用泛型,就可以保证存入和取出的都是String类型, 不必在进行cast了。比如:

  1. List<String> l = new ArrayList<>();
  2. l.add("abc");
  3. String s = l.get(0);

泛型的使用

  1. 定义类/接口:
  1. public class Test<T> {
  2. private T obj;
  3. public T getObj() {
  4. return obj;
  5. }
  6. public void setObj(T obj) {
  7. this.obj = obj;
  8. }
  9. }
  1. 使用方式:
  2. List<String> l = new ArrayList<>( );
  3. 重点说明:
  4. 变量类型中的泛型,和实例类型中的泛型,必须保证相同(不支持继承关系)。
  5. 既然有了这个规定, 因此在JDK1.7时就推出了一个新特性叫菱形泛型(The Diamond), 就是说后面的泛型可以省略直接写成<>, 反正前后一致。
  1. 定义方法:
  1. public <Q extends Object,T> void print(Q q) {
  2. System.out.println(q);
  3. }
  1. 说明:
  2. 泛型的声明,必须在方法的修饰符(public,static,final,abstract等)之后,返回值声明之前。
  3. 方法参数列表,以及方法体中用到的所有泛型变量,都必须声明。
  4. 使用方式:
  5. 太简单,不说了。

泛型中的通配符

  1. 作用:规定只允许某一部分类作为泛型;

  2. 分类:

    无边界通配符(<?>):
      无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。
    固定上边界通配符(<? extends E>):
      使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据。
    要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界. 注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类.
    固定下边界通配符(<? super E>):
      使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据。
    要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界.

    注意: 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界。

  3. 使用方法:

3.1 无边界通配符:

  1. public static void printList(List<?> list) {
  2. for (Object o : list) {
  3. System.out.println(o);
  4. }
  5. }
  6. public static void main(String[] args) {
  7. List<String> l1 = new ArrayList<>();
  8. l1.add("aa");
  9. l1.add("bb");
  10. l1.add("cc");
  11. printList(l1);
  12. List<Integer> l2 = new ArrayList<>();
  13. l2.add(11);
  14. l2.add(22);
  15. l2.add(33);
  16. printList(l2);
  1. 注意:
  2. 这里的printList方法不能写成public static void printList(List<Object> list)的形式。
  3. 原因在上文提到过,变量类型中的泛型,和实例类型中的泛型,必须保证相同。两者之间不支持继承关系。
  4. 重点说明:我们不能对List<?>使用addget以及List拥有的其他方法。
  5. 原因是,我们不确定该List的类型, 也就不知道add,或者get方法的参数类型。
  6. 但是也有特例。
  7. 请看下面代码:
  1. public static void addTest(List<?> list) {
  2. Object o = new Object();
  3. // list.add(o); // 编译报错
  4. // list.add(1); // 编译报错
  5. // list.add("ABC"); // 编译报错
  6. list.add(null); // 特例
  7. // String s = list.get(0); // 编译报错
  8. // Integer i = list.get(1); // 编译报错
  9. Object o = list.get(2); // 特例
  10. }

这个地方有点不好理解。

我们可以假设:使用这些方法编译不报错。

以上面的代码为例,并且取消上面的注释。

由于参数的泛型不确定,调用者可能会传List<Number>,也可能传List<String>。
当调用者传过来的参数是List<Interger>,执行到list.add(o)以及list.(“ABC”)的时候,系统肯定会抛出异常,使得后面的代码无法执行。

所以,编译器其实是把运行时可能出现的异常放在编译阶段来检查,提高了代码的健壮性以及安全性。

  1. 固定上边界通配符:
  1. public static double sumOfList(List<? extends Number> list) {
  2. double s = 0.0;
  3. for (Number n : list) {
  4. // 注意这里得到的n是其上边界类型的, 也就是Number,需要将其转换为double.
  5. s += n.doubleValue();
  6. }
  7. return s;
  8. }
  9. public static void main(String[] args) {
  10. List<Integer> list1 = Arrays.asList(1, 2, 3, 4);
  11. System.out.println(sumOfList(list1));
  12. List<Double> list2 = Arrays.asList(1.1, 2.2, 3.3, 4.4);
  13. System.out.println(sumOfList(list2));
  14. }
  1. 重点说明:我们不能对List<? extends E>使用add方法。
  2. 原因是,我们不确定该List的类型, 也就不知道add方法的参数类型。
  3. 但是也有特例。
  4. 请看下面代码:
  1. public static void addTest2(List<? extends Number> l) {
  2. // l.add(1); // 编译报错
  3. // l.add(1.1); // 编译报错
  4. l.add(null);
  5. Number number = l.get(1); // 正常
  6. }

目的跟第一种通配符类似,就是编译器其实是把运行时可能出现的异常放在编译阶段来检查。

但是,我们可以保证不管参数是什么泛型,里面的元素肯定是Number或者其子类,所以,从List中获取一个Number元素的get()方法是允许的。

  1. 固定下边界通配符:
  1. public static void addNumbers(List<? super Integer> list) {
  2. for (int i = 1; i <= 10; i++) {
  3. list.add(i);
  4. }
  5. }
  6. public static void main(String[] args) {
  7. List<Object> list1 = new ArrayList<>();
  8. addNumbers(list1);
  9. System.out.println(list1);
  10. List<Number> list2 = new ArrayList<>();
  11. addNumbers(list2);
  12. System.out.println(list2);
  13. List<Double> list3 = new ArrayList<>();
  14. // addNumbers(list3); // 编译报错
  15. }
  1. 重点说明:我们不能对List<? extends E>使用get方法。
  2. 原因是,我们不确定该List的类型, 也就不知道add方法的参数类型。
  3. 但是也有特例。
  4. 请看下面代码:
  1. public static void getTest2(List<? super Integer> list) {
  2. // Integer i = list.get(0); //编译报错
  3. Object o = list.get(1);
  4. }

目的跟第一种通配符类似,就是编译器其实是把运行时可能出现的异常放在编译阶段来检查。

但是,我们可以保证不管参数是什么泛型,里面的元素肯定是Integer,所以,从List中add一个Integer元素的add()方法是允许的。

  1. 典型使用场景:
  2. 使用<? super E>有个常见的场景就是Comparator
  3. TreeSet有这么一个构造方法:TreeSet(Comparator<? super E> comparator) ,就是使用Comparator来创建TreeSet
  4. 请看下面的代码:
  1. import java.util.Comparator;
  2. import java.util.TreeSet;
  3. class Person {
  4. private String name;
  5. private int age;
  6. public Person(String name, int age) {
  7. this.name = name;
  8. this.age = age;
  9. }
  10. public String getName() {
  11. return name;
  12. }
  13. public void setName(String name) {
  14. this.name = name;
  15. }
  16. public int getAge() {
  17. return age;
  18. }
  19. public void setAge(int age) {
  20. this.age = age;
  21. }
  22. }
  23. class Student extends Person {
  24. public Student(String name, int age) {
  25. super(name, age);
  26. }
  27. }
  28. class comparatorTest1 implements Comparator<Person> {
  29. @Override
  30. public int compare(Person s1, Person s2) {
  31. int num = s1.getAge() - s2.getAge();
  32. return num == 0 ? s1.getName().compareTo(s2.getName()) : num;
  33. }
  34. }
  35. public class Test {
  36. public static void main(String[] args) {
  37. TreeSet<Student> ts2 = new TreeSet<>(new comparatorTest1());
  38. ts2.add(new Student("Susan", 23));
  39. ts2.add(new Student("Rose", 27));
  40. ts2.add(new Student("Jane", 19));
  41. for (Student stu : ts2) {
  42. System.out.println(stu.getName() + ":" + stu.getAge());
  43. }
  44. }
  45. }
  1. 注意:
  2. 在上述代码中,构造方法TreeSet(Comparator<? super E> comparator)中的E,来源于泛型类引用 TreeSet<Student> ts2
  3. 因为泛型限定是<? super E>,即<? super Student>,所以Comparator的泛型必须是Student的父类,即Person

总结:

有人将上面的原则总结了一下,写作”in out”原则, 归纳起来就是:

  1. in或者producer就是你要读取出数据以供随后使用(想象一下Listget), 这时使用extends关键字, 固定上边界的通配符. 你可以将该对象当做一个只读对象;
  2. out或者consumer就是你要将已有的数据写入对象(想象一下Listadd), 这时使用super关键字, 固定下边界的通配符. 你可以将该对象当做一个只能写入的对象;
  3. 当你希望inproducer的数据能够使用Object类中的方法访问时, 使用无边界通配符;
  4. 当你需要一个既能读又能写的对象时, 就不要使用通配符了.
你的支持将鼓励作者继续创作

评论(0)

(无)