Java 基础1 - 泛型

泛型

泛型是什么?泛型可以理解为一种宽泛的类型,具体是哪一种并没有限制。

泛型类

声明一个容纳 String 类型的 List 可以这样写:

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

这样代表 list 是一个列表,而且是可以容纳 String 类型的,尖括号里面的 String 起到了说明和限制的作用。其实里面不止可以写 String ,还可以写 Integer 或者自定义的其他类甚至是泛型类来作为元素类型,所以 List 是一个泛型容器。

下面展示一个泛型的应用。

public class Pair<T> {
  T first;
  T second;

  public Pair(T first, T second) {
    this.first = first;
    this.second = second;
  }

  public T getFirst() {
    return this.first;
  }

  public T getSecond() {
    return this.second;
  }
}

在这里编写 Pair 的时候,Pair 后面加了 <T>表明这是一个泛型类,它有属性 firstsecond,这两个属性的类型都是 T,构造函数的两个变量类型也是 TT 可以是随意一种类型(但是当然不能是基本类型啦),而且,Pair 的两个实例方法的返回类型也用了T。

这样写的结果就是:实例化时构造函数传的什么类型,T就是什么类型。接下来试一下:

Pair<Integer> pair = new Pair<Integer>(1, 2);
Integer f = pair.getFirst();
Integer s = pair.getSecond();
System.out.println(f); // 输出1
System.out.println(s); // 输出2

可以看到可以直接用Integer类型的变量来接收 getFirstgetSecond 的返回值。可以自行试一下 String 类型的效果。

一个泛型类只能有一个 T 来限制吗?其实可以有多个,接下来改造一下这个 Pair

public class Pair<T, V> {
  T first;
  V second;

  public Pair(T first, V second) {
    this.first = first;
    this.second = second;
  }

  public T getFirst() {
    return this.first;
  }

  public V getSecond() {
    return this.second;
  }
}

这次多了个 V,用它来限制 second。以下是试用:

Pair<Integer, String> pair = new Pair<>(1, "AAA");
Integer f = pair.getFirst();
String s = pair.getSecond();
System.out.println(f); // 输出1
System.out.println(s); // 输出AAA

注意:其实 TV 的类型可以一样,Pair<String, String> pair = new Pair<>("111", "AAA"); 也是可以的。

通过以上的例子可以看出,使用泛型的时候,就是把类型当做参数放进了泛型类后边的那个尖括号中,就像平时调用某个方法时把参数放进了方法的圆括号一样。

其实以上的 Pair 类也可以用不用泛型,firstsecondObject 来表示也是可以实现的,这样代码就会变成:

public class Pair {
  Object first;
  Object second;

  public Pair(Object first, Object second) {
    this.first = first;
    this.second = second;
  }

  public Object getFirst() {
    return this.first;
  }

  public Object getSecond() {
    return this.second;
  }
}

// 试用
public static void main(String[] args) throws Exception {
    Pair pair = new Pair(1, "AAA");
    Integer f = (Integer)pair.getFirst();
    String s = (String)pair.getSecond();
    System.out.println(f); // 输出1
    System.out.println(s); // 输出AAA
}

效果确实是一样的,但是可以看到代码中出现了强制类型转换,除了代码变繁琐以外,很明显的一个问题就是万一强转错了就会导致异常出现,然而这肯定是不希望发生的情况。并且,这种问题不会在写代码的时候被 IDE 发现,因为它以为你知道你在做什么。

原理:Java 实现泛型的原理其实也是强制类型转换。对于泛型类,编译器会把泛型代码替换为非泛型代码,将泛型参数T替换为Object,这个过程叫做类型擦除,并加入必要的强制类型转换,等轮到虚拟机执行的时候,是不知道泛型这回事的。

泛型方法

一个方法也可以是支持泛型的,而且跟它所在的类是不是泛型没有关系。

    // 泛型方法
    public static <T> int indexOf(T[] arr, T elem) {
        for (int i = 0; i < arr.length; i++) {
          if (arr[i].equals(elem)) {
            return i;
          }
        }

        return -1;
      }
    // 试用
    public static void main(String[] args) throws Exception {
        String[] strs = new String[] { "A", "B", "C" };
        int index = indexOf(strs, "C");
        System.out.println(index); // 输出2

        Integer[] ints = new Integer[] { 1, 2 };
        int index2 = indexOf(ints, 1);
        System.out.println(index2); // 输出0
    }

这里编写了一个支持泛型的方法,在它的返回类型前边加了个<T> 就可以表示这是个泛型方法,这个方法的入参就是泛型的,返回类型为整型。可以看到试用的代码中传入的数组类型是可以不限定的。

泛型接口

有了前边的基础,可以直接上代码:

// 接口类Compare
package Interfaces;

public interface Compare<T> {
  boolean eq(T target);
}

// People类
import Interfaces.Compare;
public class People implements Compare<People> {
  private String name;

  public People(String name) {
    this.name = name;
  }

  @Override
  public boolean eq(People target) {
    return this.name.equals(target.name);
  }

}

// 测试
public class App {
    public static void main(String[] args) throws Exception {
        People p1 = new People("A");
        People p2 = new People("A");
        People p3 = new People("B");
        System.out.println(p1.eq(p2)); // TRUE
        System.out.println(p1.eq(p3)); // FALSE
    }
}

这声明了一个 Compare 接口,它有一个 eq() 方法用于判断两个对象是否相等,至于对象的具体类型是未知的,使用泛型 T 来代表。

People 类实现了这个接口,注意实现的时候在 Compare 后边尖括号指明了是 People 类,所以在实现eq() 方法的时候,参数传的就是一个 People 类型的 target

泛型的限定

Basic

前边写的泛型都是代表可以传入任意类型,但是其实是不安全的,假如有个泛型方法涉及对象中 length 的访问,这个方法很明显不能适用于 Integer 类型的对象,因为它没有这个属性,这时候需要对泛型对象进行类型的限制。

类型限制是有上界下界的,可以理解为范围

  1. 上界示例

    public class App {
        public static void main(String[] args) throws Exception {
            XM xm = new XM(1);
            People p = new People(2);
            add(xm, p); // 输出3
    
            Integer a = new Integer(1);
            add(xm, a); // 报错:reason: no instance(s) of type variable(s) exist so that Integer conforms to People
        }
    
        public static <T extends People> void add(T a, T b) {
            System.out.println(a.getAge() + b.getAge());
        }
    }
    
    class XM extends People{
        public XM(int age) {
            super(age);
        }
    }
    
    class People {
        private int age;
        public People(int age) {
            this.age = age;
        }
        public int getAge() {
            return this.age;
        }
    }
    

    以上代码中,add() 方法前边的泛型写成了:<T extends People> ,代表T所能支持的类型范围最高只能到 People 。它的意思是,T 这个类型是People 或者 People 的子类,不能是其他的。XMPeople 的子类,所以 XMPeople 的对象都可以传入 add 方法内计算,而 Integer 就会报错说不能转换成 People

  2. 下界示例

    详见通配符。

通配符

通配符是一种简写的方式。

1.<?>

比如以下两端代码效果是一样的:

    public static <T> void outAll(List<T> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.getClass());
        }
    }

    public static void outAll(List<?> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.getClass());
        }
    }

通配符的看上去比类型参数要简洁一点。

2.<? super E>

关于泛型限定请看:

import java.util.ArrayList;
import java.util.List;

public class App {
    public static void main(String[] args) throws Exception {
        List<XM> xmList = new ArrayList<>();
        XM xm = new XM(1);
        xmList.add(xm);
        outAll(xmList); // 输出1

        List<People> peopleList = new ArrayList<>();
        People p = new People(2);
        peopleList.add(p);
        outAll(peopleList); // 输出2

    }

    public static void outAll(List<? extends People> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i).getAge());
        }
    }
//    public static <T extends People> void outAll(List<T> list) {
//        for (int i = 0; i < list.size(); i++) {
//            System.out.println(list.get(i).getAge());
//        }
//    }

}

这里写了两个 List ,一个装 XM ,一个装 People 。在 outAll() 方法的入参用了 <? extends People>,它也可以限定元素类型为 People 或者其子类。

其实,这里完全可以用上一节的写法代替本节的通配符写法(比如注释那一段),只是通配符可以更简洁一点。然而并不是所有通配符写法都可以被这样替换的,详情见超类型通配符。

3.<? super E>超类型通配符

这个就有点下界的意思了。

import java.util.ArrayList;
import java.util.List;

public class App {
    public static void main(String[] args) throws Exception {
        List<XM> xmList = new ArrayList<>();
        XM xm = new XM(1);
        xmList.add(xm);
        outAll(xmList); // 输出class XM

        List<People> peopleList = new ArrayList<>();
        People p = new People(2);
        peopleList.add(p);
        outAll(peopleList); // 输出class People


        Object o = new Object();
        List<Object> list = new ArrayList();
        list.add(o);
        outAll(list); // 输出class java.lang.Object

        XMM xmm = new XMM(3); // XMM 继承了XM
        List<XMM> xmmList = new ArrayList();
        list.add(xmm);
        outAll(xmmList); // 报错
    }

    public static void outAll(List<? super XM> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i).getClass());
        }
    }

}

这里把 extends 换成了 super ,代表 List 中的元素类型只能是 XM 或者 XM 的父类,不能再比 XM 低了,所以对于 XMM 这个继承了 XM 的类来讲就会报错,方法不接受这种类型的集合。

看个例子:

import java.util.ArrayList;
import java.util.List;

public class App {
    public static void main(String[] args) throws Exception {
        List<Parent> pList = new ArrayList<>();
        pList.add(new Parent("p1"));

        List<Child> cList = new ArrayList<>();
        cList.add(new Child("c1"));

        outAll(pList); // OK
        outAll(cList); // 报错
    }

    public static <T extends Compare<T>> void outAll(List<T> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.getClass());
        }
    }

}

class Parent implements Compare<Parent> {
    public String name;
    public Parent(String name) {
        this.name = name;
    }

    @Override
    public boolean eq(Parent target) {
        return this.name.equals(target.name);
    }
}

class Child extends Parent {
    public Child(String name) {
        super(name);
    }
}

interface Compare<T> {
    boolean eq(T target);
}

父类实现了 Compare 接口,子类也可以调用接口内同名的方法,但是在 outAll() 方法却不接受 List<Child>这中类型的参数,原因就在于 <T extends Compare<T>> 的意思是:T 实现了 Compare 接口,而且是和同种类型作比较。

父类的实现是 Compare<Parent> 而子类却没有实现 Compare<Child> 所以不被接受,子类实现的其实也是 Compare<Parent>

解决办法就是把 <T extends Compare<T>> 改为 <T extends Compare<? super T>> ,改过之后就可以接纳类实现 Compare<Parent> 这种情况了。可以看到通配符写法也并不总是可以被替换的

总结

上述三种的区别如下:

  1. 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象,它不能被类型参数形式替代。
  2. 和用于灵活读取,使得方法可以读取E或E的任意子类型的容器对象,它们可以用类型参数的形式替代,但通配符形式更为简洁。
  3. 可以去找些容器类研究下

ATTENTION

  1. 获取类的信息

    可以使用 Integer.class 但不能使用 Pair<integer>.class。因为这个类型对象只有一份,与泛型无关。以下可以验证:

    Pair<Integer, Integer> pair1 = new Pair<>(1, 2);
    Pair<Integer, String> pair2 = new Pair<>(1, "AAA");
    System.out.println(pair1.getClass() == Pair.class); // true
    System.out.println(pair2.getClass() == Pair.class); // true
    
  2. instanceof 不适用于泛型

    以下是不支持的:

    pair1 instanceof Pair<Integer, Integer>;
    
  3. 接口

    之前有过两个类,父类的实现是 Compare<Parent> ,子类实现的其实也是 Compare<Parent> 。此时想让子类再实现这个接口是不行的,下面的代码是会报错的:

    class Child extends Parent implements Compare<Child> {
        public Child(String name) {
            super(name);
        }
    }
    

    Compare 接口不能被实现两次,且两次实现的类型参数还不同,一次是 Compare<Parent> ,一次是 Compare<Child> 。为什么不允许呢?因为类型擦除后,实际上只能有一个。

    解决办法是直接重写父类的 eq() 方法即可。

  4. 泛型类型不能实例化

    某个参数是 T 类型的,不可以通过 new T() 来实例化。

  5. 不能创建泛型类数组

    以下是非法的:

    Pair<Integer, Integer>[] arr = new Pair<Integer, Integer>[] {
      new Pair<>(1, 2), new Pair<>(3, 4)
    };
    

    原因如下:

    Integer[] ints = new Integer[2];
    Object[] objs = ints;
    objs[0] = "aaa";  // 编译是没有问题的,运行时会抛出ArrayStoreException,因为Java知道实际的类型是Integer,所以写入String会抛出异常。
    
    Pair<Integer, Integer>[] arr = new Pair<Integer, Integer>[] {
      new Pair<>(1, 2), new Pair<>(3, 4)
    };
    Object[] objs2 = arr;
    objs2[0] = new Pair<Double, String>(3.1d, "aaa"); // 这里在运行的时候什么错误都不会发生
    

    因为 Pair<Double, String> 的运行时类型是 Pair ,和 objs2 的运行时类型 Pair[] 是匹配的。但其实它的实际类型是不匹配的,在程序的其他地方,当把 objs2[0] 作为 Pair<Integer, Integer> 进行处理的时候,一定会触发异常。

    可以使用原始类型创建数组存放泛型对象:

    Pair[] arr = new Pair[] {
     new Pair<Integer, Integer>(1, 2), new Pair<Double, String>(3.1d, "aaa")
    };
    

本文作者:为何匆匆

本文链接:https://www.cnblogs.com/nyfblog/p/16519103.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   为何匆匆  阅读(64)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起