Java泛型详解

原文地址https://zhuanlan.zhihu.com/p/28242753

泛型是什么?

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


泛型的好处

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

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

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


泛型的使用

1. 定义类/接口:

public class Test<T> {

   private T obj;

   public T getObj() {
     return obj;
   }

    public void setObj(T obj) {
       this.obj = obj;
   }
}
  • 使用方式:
    List<String> l = new ArrayList<>( );
  • 重点说明:
    • 变量类型中的泛型,和实例类型中的泛型,必须保证相同(不支持继承关系)。
    • 既然有了这个规定, 因此在JDK1.7时就推出了一个新特性叫菱形泛型(The Diamond), 就是说后面的泛型可以省略直接写成<>, 反正前后一致。

2. 定义方法:

public <Q extends Object,T> void print(Q q) {
        System.out.println(q);
}
  • 说明:
    • 泛型的声明,必须在方法的修饰符(public,static,final,abstract等)之后,返回值声明之前。
    • 方法参数列表,以及方法体中用到的所有泛型变量,都必须声明。
  • 使用方式:
    太简单,不说了。


泛型中的通配符

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

2. 分类:

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

3. 使用方法:

3.1 无边界通配符:

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

这个地方有点不好理解。

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

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

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

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


2. 固定上边界通配符:

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

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

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

3. 固定下边界通配符:

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

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

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

  • 典型使用场景:
    使用<? super E>有个常见的场景就是Comparator。
    TreeSet有这么一个构造方法:TreeSet(Comparator<? super E> comparator) ,就是使用Comparator来创建TreeSet。
    请看下面的代码:
import java.util.Comparator;
import java.util.TreeSet;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

class Student extends Person {
    public Student(String name, int age) {
        super(name, age);
    }
}

class comparatorTest1 implements Comparator<Person> {
    @Override
    public int compare(Person s1, Person s2) {
        int num = s1.getAge() - s2.getAge();
        return num == 0 ? s1.getName().compareTo(s2.getName()) : num;
    }
}

public class Test {
    public static void main(String[] args) {

        TreeSet<Student> ts2 = new TreeSet<>(new comparatorTest1());
        ts2.add(new Student("Susan", 23));
        ts2.add(new Student("Rose", 27));
        ts2.add(new Student("Jane", 19));
        for (Student stu : ts2) {
            System.out.println(stu.getName() + ":" + stu.getAge());
        }

    }
}


注意:
通过查看TreeSet源码得知,构造方法TreeSet(Comparator<? super E> comparator)中的E,来源于泛型类 TreeSet<E>,在这里就是变量ts2的类型TreeSet<Student> 中的Student 。
因为泛型限定是<? super E>,即<? super Student>,所以Comparator的泛型必须是Student的父类,即Person。

总结:

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

  • in或者producer就是你要读取出数据以供随后使用(想象一下List的get), 这时使用extends关键字, 固定上边界的通配符. 你可以将该对象当做一个只读对象;
  • out或者consumer就是你要将已有的数据写入对象(想象一下List的add), 这时使用super关键字, 固定下边界的通配符. 你可以将该对象当做一个只能写入的对象;
  • 当你希望in或producer的数据能够使用Object类中的方法访问时, 使用无边界通配符;
  • 当你需要一个既能读又能写的对象时, 就不要使用通配符了.

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java泛型Java 5引入的新特性,可以提高代码的可读性和安全性,降低代码的耦合度。泛型是将类型参数化,实现代码的通用性。 一、泛型的基本语法 在声明类、接口、方法时可以使用泛型泛型的声明方式为在类名、接口名、方法名后面加上尖括号<>,括号可以声明一个或多个类型参数,多个类型参数之间用逗号隔开。例如: ```java public class GenericClass<T> { private T data; public T getData() { return data; } public void setData(T data) { this.data = data; } } public interface GenericInterface<T> { T getData(); void setData(T data); } public <T> void genericMethod(T data) { System.out.println(data); } ``` 其,`GenericClass`是一个泛型类,`GenericInterface`是一个泛型接口,`genericMethod`是一个泛型方法。在这些声明,`<T>`就是类型参数,可以用任何字母代替。 二、泛型的使用 1. 泛型类的使用 在使用泛型类时,需要在类名后面加上尖括号<>,并在括号指定具体的类型参数。例如: ```java GenericClass<String> gc = new GenericClass<>(); gc.setData("Hello World"); String data = gc.getData(); ``` 在这个例子,`GenericClass`被声明为一个泛型类,`<String>`指定了具体的类型参数,即`data`字段的类型为`String`,`gc`对象被创建时没有指定类型参数,因为编译器可以根据上下文自动推断出类型参数为`String`。 2. 泛型接口的使用 在使用泛型接口时,也需要在接口名后面加上尖括号<>,并在括号指定具体的类型参数。例如: ```java GenericInterface<String> gi = new GenericInterface<String>() { private String data; @Override public String getData() { return data; } @Override public void setData(String data) { this.data = data; } }; gi.setData("Hello World"); String data = gi.getData(); ``` 在这个例子,`GenericInterface`被声明为一个泛型接口,`<String>`指定了具体的类型参数,匿名内部类实现了该接口,并使用`String`作为类型参数。 3. 泛型方法的使用 在使用泛型方法时,需要在方法名前面加上尖括号<>,并在括号指定具体的类型参数。例如: ```java genericMethod("Hello World"); ``` 在这个例子,`genericMethod`被声明为一个泛型方法,`<T>`指定了类型参数,`T data`表示一个类型为`T`的参数,调用时可以传入任何类型的参数。 三、泛型的通配符 有时候,我们不知道泛型的具体类型,可以使用通配符`?`。通配符可以作为类型参数出现在方法的参数类型或返回类型,但不能用于声明泛型类或泛型接口。例如: ```java public void printList(List<?> list) { for (Object obj : list) { System.out.print(obj + " "); } } ``` 在这个例子,`printList`方法的参数类型为`List<?>`,表示可以接受任何类型的`List`,无论是`List<String>`还是`List<Integer>`都可以。在方法内部,使用`Object`类型来遍历`List`的元素。 四、泛型的继承 泛型类和泛型接口可以继承或实现其他泛型类或泛型接口,可以使用子类或实现类的类型参数来替换父类或接口的类型参数。例如: ```java public class SubGenericClass<T> extends GenericClass<T> {} public class SubGenericInterface<T> implements GenericInterface<T> { private T data; @Override public T getData() { return data; } @Override public void setData(T data) { this.data = data; } } ``` 在这个例子,`SubGenericClass`继承了`GenericClass`,并使用了相同的类型参数`T`,`SubGenericInterface`实现了`GenericInterface`,也使用了相同的类型参数`T`。 五、泛型的限定 有时候,我们需要对泛型的类型参数进行限定,使其只能是某个类或接口的子类或实现类。可以使用`extends`关键字来限定类型参数的上限,或使用`super`关键字来限定类型参数的下限。例如: ```java public class GenericClass<T extends Number> { private T data; public T getData() { return data; } public void setData(T data) { this.data = data; } } public interface GenericInterface<T extends Comparable<T>> { T getData(); void setData(T data); } ``` 在这个例子,`GenericClass`的类型参数`T`被限定为`Number`的子类,`GenericInterface`的类型参数`T`被限定为实现了`Comparable`接口的类。 六、泛型的擦除 在Java泛型信息只存在于代码编译阶段,在编译后的字节码会被擦除。在运行时,无法获取泛型的具体类型。例如: ```java public void genericMethod(List<String> list) { System.out.println(list.getClass()); } ``` 在这个例子,`list`的类型为`List<String>`,但是在运行时,`getClass`返回的类型为`java.util.ArrayList`,因为泛型信息已经被擦除了。 七、泛型的类型推断 在Java 7,引入了钻石操作符<>,可以使用它来省略类型参数的声明。例如: ```java List<String> list = new ArrayList<>(); ``` 在这个例子,`ArrayList`的类型参数可以被编译器自动推断为`String`。 八、总结 Java泛型是一个强大的特性,可以提高代码的可读性和安全性,降低代码的耦合度。在使用泛型时,需要注意它的基本语法、使用方法、通配符、继承、限定、擦除和类型推断等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值