【Java学习笔记六】——一文教你入门泛型

声明:本文章内容主要摘选自尚硅谷宋红康Java教程、《Java核心卷一》、廖雪峰Java教程,示例代码部分出自本人,更多详细内容推荐直接观看以上教程及书籍,若有错误之处请指出,欢迎交流。

一、简单定义泛型类

1.必要性

  • 在Java中增加泛型类之前,泛型程序设计是用继承实现的。ArrayList类只维护一个Object引用的数组:
public class ArragList
{
      public Object[] elementData;
      ...
      public object get(int i){};
      public void add(Object o){};
}
  • 这种方法有两个问题。当获取一个值时必须进行强制类型转换。

    Arraylist files=new Arraylist o;
    String filenane=(String)files.get(o);
    
  • 此外,这里没有错误检查。可以向数组列表中添加任何类的对象。

  • files.add(new File("..."));对于这个调用,编译和运行都不会出错。然而在其他地方,如果将get的结果强制类型转换为String类型,就会产生一个错误。

  • 泛型提供了一个更好的解决方案:类型参数(type parameters)。ArrayList类有一个类型参数用来指示元素的类型:

    Arraylist<String> files = new Arraylist<String>;
    
  • 这使得代码具有更好的可读性和安全性。人们一看就知道这个数组列表中包含的是String对象。

2.简单定义泛型类

一个泛型类(generic class)就是具有一个或多个类型变量的类。我们使用一个简单的Order类作为例子。对于这个类来说,我们只关注泛型,而不会为数据存储的细节烦恼。

class Order<T> {

    String orderName;
    int orderId;

    T orderT;

    public Order(){};

    public Order(String orderName, int orderId, T orderT) {
        this.orderName = orderName;
        this.orderId = orderId;
        this.orderT = orderT;
    }

    public T getOrderT() {
        return orderT;
    }
    public void setOrderT(T orderT){
        this.orderT = orderT;
    }
}

//使用泛型后,我们可以任意定义Order类的类型
Order<String> o1 = new Order<>();
Order<Integer> o2 = new Order<>();

//如果定义了泛型类,实例化没有指明类的泛型,则认为此泛型类型为0bject类型
Order o3 = new Order();

二、使用泛型

1.使用泛型

使用ArrayList时,如果不定义泛型类型时,泛型类型实际上就是Object:

// 编译器警告:
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);

此时,只能把<T>当作Object使用,没有发挥泛型的优势。
当我们定义泛型类型<String>后,List<T>的泛型接口变为强类型List<String>

// 无编译器警告:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
// 无强制转型:
String first = list.get(0);
String second = list.get(1);

当我们定义泛型类型<Number>后,List<T>的泛型接口变为强类型List<Number>

List<Number> list = new ArrayList<Number>();
list.add(new Integer(123));
list.add(new Double(12.34));
Number first = list.get(0);
Number second = list.get(1);

编译器看到泛型类型List<Number>就可以自动推断出后面的ArrayList<T>的泛型类型必须是ArrayList<Number>,因此,可以把代码简写为:

  List<Number> list = new ArrayList<>();

2.泛型接口

除了ArrayList使用了泛型,还可以在接口中使用泛型。例如,Arrays.sort(Object[])可以对任意数组进行排序,但待排序的元素必须实现Comparable<T>这个泛型接口:

我们可以直接对String数组进行排序:

String[] ss=new String[]{"orange","Apple","Pear"}; 
Arrays.sort(ss);
System.out.println(Arrays.toString(ss));

这是因为String本身已经实现了Comparable<String>接口。如果换成我们自定义的Person类型则需要让Person实现Comparable<T>接口:

class Person implements Comparable<Person>{
    private String name;
    private int age;
    private double salary;

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

    public int compareTo(Person other)
    {
        return this.name.compareTo(other.name);//按照姓名从小到大排列,若加负号则为从大到小;若将name改变成其他属性如age即可改变排序依据
     }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", salary=" + salary +
                '}' + '\n';
    }
}
@Test      //单元测试方法
    public void test1(){
        Person[] ps=new Person[]{
                new Person("Bob",61,1000),
                new Person("Alice",88,3000),
                new Person("Lily",75,2000),
        };
        Arrays. sort(ps);
        System. out. println(Arrays. toString(ps));
    }

拓展:除了Comparable<T>接口,还有Comparator接口

注:Comparator接口的使用:定制排序
1.背景:
当元素的类型没有实现Java.Lang.Comparable接口而又不方便修改代码,或者实现了java.Lang.Comparable接口的排序规定不适合当前的操作,么可以考感使用Comparator 的对象来排序.
2.重写compare(Object o1,Object o2)方法,比较o1和o2的大小:
如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。

public void test() {
        String[] str = {"AA", "II", "GG", "CC", "EE"};
        Arrays.sort(str, new Comparator() {
            @Override
            public int compare(Object o1, Object o2){
                if(o1 instanceof String && o2 instanceof String){
                    String s1 = (String)o1;
                    String s2 = (String)o2;
                    return -s1.compareTo(s2);//如果不加负号就是从小到大
                }
                throw new RuntimeException("输入的数据类型不一致");
            }
        });
        System.out.println(Arrays.toString(str));
    }

三、泛型继承

一个类可以继承自一个泛型类。例如:父类的类型是Pair,子类的类型是IntPair,可以这么继承:

  public class IntPair extends Pair<Integer> {
  }

使用的时候,因为子类IntPair并没有泛型类型,所以,正常使用即可:

  IntPair ip = new IntPair(1, 2);

四、通配符

1.extends通配符

我们前面已经讲到了泛型的继承关系:Pair不是Pair的子类。
假设我们定义了Pair

  public class Pair<T> { ... }

然后,我们又针对Pair类型写了一个静态方法,它接收的参数类型是Pair

public class PairHelper {
    static int add(Pair<Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        return first.intValue() + last.intValue();
    }
}

上述代码是可以正常编译的。使用的时候,我们传入:

  int sum = PairHelper.add(new Pair<Number>(1, 2));

注意:传入的类型是Pair,实际参数类型是(Integer, Integer)。

既然实际参数是Integer类型,试试传入Pair

public static void main(String[] args){
      Pair<Integer>p=new Pair<>(123,456); 
      int n=add(p); 
      System.out.println(n); 
}
static int add(Pair<Number>p){
      Number first=p.getFirst(); 
      Number last=p.getLast(); 
      return first.intValue()+last.intValue();
}
/*
直接运行,会得到一个编译错误:
incompatible types: Pair<Integer> cannot be converted to Pair<Number>
原因很明显,因为Pair<Integer>不是Pair<Number>的子类,因此,add(Pair<Number>)不接受参数类型Pair<Integer>。
问题在于方法参数类型定死了只能传入Pair<Number>。

此时使用Pair<? extends Number>使得方法接收所有泛型类型为Number或Number子类的Pair类型。我们把代码改写如下:*/
static int add(Pair<? extends Number>p){
      Number first=p.getFirst(); 
      Number last=p.getLast(); 
      return first.intValue()+last.intValue();
}

这样一来,给方法传入Pair类型时,它符合参数Pair<? extends Number>类型。这种使用<? extends Number>的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。

除了可以传入Pair<Integer>类型,我们还可以传入Pair<Double>类型,Pair<BigDecimal>类型等等,因为Double和BigDecimal都是Number的子类。

如果我们考察对Pair<? extends Number>类型调用getFirst()方法,实际的方法签名变成了:

  <? extends Number> getFirst();

即返回值是Number或Number的子类,因此,可以安全赋值给Number类型的变量:

  Number x = p.getFirst();

然后,我们不可预测实际类型就是Integer,例如,下面的代码是无法通过编译的:

  Integer x = p.getFirst();

这是因为实际的返回类型可能是Integer,也可能是Double或者其他类型,编译器只能确定类型一定是Number的子类(包括Number类型本身),但具体类型无法确定。

2.super通配符

我们前面已经讲到了泛型的继承关系:Pair不是Pair的子类。

考察下面的set方法:

void set(Pair<Integer> p, Integer first, Integer last) {
    p.setFirst(first);
    p.setLast(last);
}

传入Pair<Integer>是允许的,但是传入Pair<Number>是不允许的。

和extends通配符相反,这次,我们希望接受Pair<Integer>类型,以及Pair<Number>、Pair<Object>,因为Number和Object是Integer的父类,setFirst(Number)和setFirst(Object)实际上允许接受Integer类型。

我们使用super通配符来改写这个方法:

void set(Pair<? super Integer> p, Integer first, Integer last) {
    p.setFirst(first);
    p.setLast(last);
}

注意到Pair<? super Integer>表示,方法参数接受所有泛型类型为Integer或Integer父类的Pair类型。

下面的代码可以被正常编译:

public static void main(String[] args){
      Pair<Number>p1=new Pair<>(12.3,4.56);
      Pair<Integer>p2=new Pair<>(123,456); 
      setSame(p1,100); 
      setSame(p2,200); 
      System. out. println(p1. getFirst()+","+pl. getLast()); 
      System. out. println(p2. getFirst()+","+p2. getLast());
}
static void setsame(Pair<? super Integer>p, Integer n){
      p.setFirst(n); 
      p.setLast(n);
}

考察Pair<? super Integer>的setFirst()方法,它的方法签名实际上是:

  void setFirst(? super Integer);

因此,可以安全地传入Integer类型。

再考察Pair<? super Integer>的getFirst()方法,它的方法签名实际上是:

  ? super Integer getFirst();

这里注意到我们无法使用Integer类型来接收getFirst()的返回值,即下面的语句将无法通过编译:

Integer x = p.getFirst();
因为如果传入的实际类型是Pair<Number>,编译器无法将Number类型转型为Integer。

注意:虽然Number是一个抽象类,我们无法直接实例化它。但是,即便Number不是抽象类,这里仍然无法通过编译。此外,传入Pair<Object>类型时,编译器也无法将Object类型转型为Integer。

唯一可以接收getFirst()方法返回值的是Object类型:

  Object obj = p.getFirst();

因此,使用<? super Integer>通配符表示:

允许调用set(? super Integer)方法传入Integer的引用;

不允许调用get()方法获得Integer的引用。

唯一例外是可以获取Object的引用:Object o = p.getFirst()。

换句话说,使用<? super Integer>通配符作为方法参数,表示方法内部代码对于参数只能写,不能读

3.无限制通配符

我们已经讨论了<? extends T>和<? super T>作为方法参数的作用。实际上,Java的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个?:

  void sample(Pair<?> p) {
  }

因为<?>通配符既没有extends,也没有super,因此:

  • 不允许调用set(T)方法并传入引用(null除外);
  • 不允许调用T get()方法并获取T引用(只能获取Object引用)。
  • 通配符有一个独特的特点,就是:Pair是所有Pair的超类

为什么要使用这样脆弱的类型?它对于许多简单的操作非常有用。例如,下面这个方法将用来测试一个pair是否包含一个null引用,它不需要实际的类型。

public static boolean hasNu1ls(Pair<?>p)
{
      return p.getFirstO==null || p.getSecond() == null;
}
//通过将hasNulls转换成泛型方法,可以避免使用通配符类型:
public static<T> boolean hasNulls(Pair<T> p)
//但是,带有通配符的版本可读性更强

拓展内容

1.对比extends和super通配符

我们再回顾一下extends通配符。作为方法参数,<? extends T>类型和<? super T>类型的区别在于:

<? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);

<? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。

一个是允许读不允许写,另一个是允许写不允许读。

先记住上面的结论,我们来看Java标准库的Collections类定义的copy()方法:

public class Collections {
    // 把src的每个元素复制到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i);
            dest.add(t);
        }
    }
}

它的作用是把一个List的每个元素依次添加到另一个List中。它的第一个参数是List<? super T>,表示目标List,第二个参数List<? extends T>,表示要复制的List。我们可以简单地用for循环实现复制。在for循环中,我们可以看到,对于类型<? extends T>的变量src,我们可以安全地获取类型T的引用,而对于类型<? super T>的变量dest,我们可以安全地传入T的引用。

这个copy()方法的定义就完美地展示了extends和super的意图:

copy()方法内部不会读取dest,因为不能调用dest.get()来获取T的引用;

copy()方法内部也不会修改src,因为不能调用src.add(T)。

这是由编译器检查来实现的。如果在方法代码中意外修改了src,或者意外读取了dest,就会导致一个编译错误:

public class Collections {
    // 把src的每个元素复制到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        ...
        T t = dest.get(0); // compile error!
        src.add(t); // compile error!
    }
}
//这个copy()方法的另一个好处是可以安全地把一个List<Integer>添加到List<Number>,但是无法反过来添加:

// copy List<Integer> to List<Number> ok:
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);
// ERROR: cannot copy List<Number> to List<Integer>:
Collections.copy(intList, numList);
//而这些都是通过super和extends通配符,并由编译器强制检查来实现的。

2.PECS原则

何时使用extends,何时使用super?为了便于记忆,我们可以用PECS原则:Producer Extends Consumer Super

即:如果需要返回T,它是生产者(Producer),要使用extends通配符;如果需要写入T,它是消费者(Consumer),要使用super通配符。

还是以Collections的copy()方法为例:

public class Collections {
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i); // src是producer
            dest.add(t); // dest是consumer
        }
    }
}

需要返回T的src是生产者,因此声明为List<? extends T>,需要写入T的dest是消费者,因此声明为List<? super T>。

此笔记仅针对有一定编程基础的同学,且本人只记录比较重要的知识点,若想要入门Java可以先行观看相关教程或书籍后再阅读此笔记。

最后附一下相关链接:
Java在线API中文手册
Java platform se8下载
尚硅谷Java教学视频
《Java核心卷一》

posted @ 2020-08-18 15:08  洛水凌云  阅读(128)  评论(0编辑  收藏  举报