Java学习之==>泛型

一、什么是泛型

  泛型,即“参数化类型”,在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

二、Java中为何要引入泛型

因为继承和多态的出现,在操作一些容器类时,需要大量的对象类型判断。先来看看下面这两段代码:

public class User {

  private Integer id;

  private String name;

  private Integer age;

  private User() {
    return;
  }

  private User(String name) {
    this();
    this.name = name;
  }

  private User(Integer id, String name) {
    this(name);
    this.id = id;
  }

  private User(Integer id, String name, Integer age) {
    this(id,name);
    this.age = age;
  }

  public static User of() {
    return new User();
  }

  public static User of(String name) {
    return new User(name);
  }

  public static User of(Integer id, String name) {
    return new User(id, name);
  }

  public static User of(Integer id, String name, Integer age) {
    return new User(id, name, age);
  }

  public Integer getId() {
    return id;
  }

  public void setId(Integer id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

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

  public Integer getAge() {
    return age;
  }

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

  @Override
  public String toString() {
    return "User{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", age=" + age +
            '}';
  }
}
User
public class Demo01 {

  public static void main(String[] args) {

    List list = new ArrayList();

    // 插入User对象
    list.add(User.of(1,"大叔",40));
    list.add(User.of(1,"木木",30));

    // 插入其他类型的对象
    list.add(123);
    list.add("abb");
    
    printUser(list);
  }

  public static void printUser(List list) {
    for (int i = 0; i < list.size(); i++) {
      User user = (User) list.get(i);
      System.out.println(user);
    }
  }
}

printUser() 方法目的是遍历打印User对象,但在 main 方法中除了可以插入 User 对象,也可以插入其他类型的对象,执行的时候必然会报错。然后我们就得在代码中加入如下判断:

public static void printUser(List list) {
  for (int i = 0; i < list.size(); i++) {
    Object obj = list.get(i);
    if (obj instanceof User) {
      User user = (User) obj;
      System.out.println(user);
    }
  }
}

这样写代码的话就非常麻烦,代码中需要添加很多判断,使用起来非常不方便,这样泛型就应运而生了,我们只需要把代码改成如下这种形式:

public class Demo01 {

  public static void main(String[] args) {

    // 这里使用泛型的作用是:list里面只能装User对象
    List<User> list = new ArrayList<>();

    // 插入User对象
    list.add(User.of(1,"大叔",40));
    list.add(User.of(1,"木木",30));

    // 如果上面List不使用泛型,这里编译时不会报错,运行时才会报错
    // list.add("Hello");
    // list.add("World");

    printUser(list);
  }

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

从以上代码可以看出,有了泛型以后,我们在代码中只需要在定义容器时给它指定一种类型,那么这个容器就只能存放该类型的对象,在业务代码中就不再需要对对象的类型进行判断,简化了很多代码的编写。泛型还可以认为是一种约定,为了使用方便,约定一个容器中只能存放某一种类型的对象。

三、泛型的使用

泛型有三种使用方式,分别是:泛型类、泛型接口和泛型方法。

1、泛型类

泛型类用于类的定义当中,通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

/**
 * 此处 K,V 可以随便写为任意标识,常见的如K、V、T、E等形式的参数常用于表示泛型
 * 在实例化泛型类时,必须指定T的具体类型,如:String、Integer等。
 */
public class Generic<K, V> {

  /**
   * key,val这个成员变量的类型分别为 K,V 这两个类型由外部指定
   */
  private K key;
  private V val;

  /**
   * 泛型类的构造方法的形参 k和v 的类型也为 K,V,同样由外部指定
   */
  public Generic(K k, V v) {
    this.key = k;
    this.val = v;
  }

  /**
   * 泛型中普通方法的返回值类型 K,V 同样由外部指定
   * 注意:以下这种不是泛型方法
   */
  public K getKey() {
    return key;
  }
  public V getVal() {
    return val;
  }
}
泛型类

但是,在使用泛型类时就一定要传入类型实参吗?在语法上是不一定的,可以不传,但是使用时最好按照约定传递,否则我们定义泛型类就没有意义了。如果不传入类型实参的话,就可以往容器内添加任何类型的对象,这样还说得在业务代码种进行类型判断。

泛型类的使用还有一种方式,使用 extends 和 super 关键字来限制我们使用传入参数的类型,如下:

/**
 * 此处 V extends Person 限制了 V 的类型只能使用 Person和它的子类
 */
public class Generic<K, V extends Person> {
  
  private K key;
  private V val;
  
  public Generic(K k, V v) {
    this.key = k;
    this.val = v;
  }
  
  public K getKey() {
    return key;
  }
  public V getVal() {
    return val;
  }
}

2、泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:

public interface Generic<K, V> {

  public K test01();
  public V test02();

}

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:public class testGeneric<K, V> implements Generic<K, V>
 * 如果不声明泛型,如:public class testGeneric implements Generic,编译器会报错
 */
public class testGeneric<K, V> implements Generic<K, V> {

  @Override
  public K test01() {
    return null;
  }

  @Override
  public V test02() {
    return null;
  }
}

当实现泛型接口的类,传入泛型实参时:

/**
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 */
public class testGeneric<String, Integer> implements Generic<String, Integer> {

  @Override
  public String test01() {
    return null;
  }

  @Override
  public Integer test02() {
    return null;
  }
}

3、泛型方法

在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。

泛型类,是在实例化类的时候指明泛型的具体类型。泛型方法,是在调用方法的时候指明泛型的具体类型 。

public class Generic<K, V> {

  private K k;

  private V v;

  /**
   * 泛型方法
   * public 与 返回值中间的 <T> 非常重要,可以理解为声明此方法为泛型方法。
   * <T>表明该方法将使用泛型类型 T,此时才可以在方法中使用泛型类型 T。
   * 与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
   * 此处的 T 与上面泛型类定义的 K 和 V 没有任何关系,可以一样,也可以不一样
   * 泛型方法可以在泛型类种定义,也可以在普通类当中定义
   */
  public <T> T getType(T type){
    return type;
  }

  /**
   * 静态泛型方法
   */
  public static <S, T> Generic<S, T> of() {
    /**
     * 类型推断
     * return new Generic<S, T>() -> return Generic Pair<>()
     */
    return new Generic<>();
  }

  /**
   * 返回值或者参数带有泛型的不是泛型方法
   */
  public K getK() {
    return k;
  }

  public V getV() {
    return v;
  }
}

泛型方法能使方法独立于类而产生变化,如果能做到,你就该尽量使用泛型方法。

泛型方法中同样支持使用 extends 和 super 关键字来限制我们使用传入参数的类型,如下:

public class testGeneric {

  public static void main(String[] args) {

    Person person = null;
    Str str = null;
    User user = null;
    
    getType(person);
    getType(str);
    // User不是Person或其子类,所以会报错
    getType(user);

  }

  /**
   * 泛型方法
   */
  public static <T extends Person> T getType(T type){
    return type;
  }
}

User不是Person或其子类,所以会报错。Str是Person的子类,所以不会报错。

4、泛型通配符

泛型通配符只能作为方法的形参使用,如下:

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

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

    list1.add("hello");
    list1.add("world");

    List<Integer> list2 = new ArrayList<>();
    list2.add(1);
    list2.add(2);

    print(list1);
    print(list2);
  }

  public static void print(List<?> list) {
    for (Object obj : list) {
      System.out.println(obj);
    }
  }
}

?代表可以接收任何类型。通配符同样可以通过 extends 和 super 关键字来限制接收的类型

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

    List<Person> list1 = new ArrayList<>();

    list1.add(new Person());
    list1.add(new Person());

    List<Str> list2 = new ArrayList<>();
    list2.add(new Str());
    list2.add(new Str());

    List<User> list3 = new ArrayList<>();
    list3.add(new User());
    list3.add(new User());

    print(list1);
    print(list2);
    print(list3); // 报错
  }

  public static void print(List<? extends Person> list) {
    for (Object obj : list) {
      System.out.println(obj);
    }
  }
}

四、类型擦除

泛型是 Java 1.5 版本才引进的概念,在这之前没有泛型的概念,但泛型代码能够很好地和之前版本的代码很好地兼容,是因为:泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。通俗地讲,泛型类和普通类在 Java 虚拟机内是没有什么特别的地方。我们来看看下面这段代码:

public class App {
  public static void main(String[] args) {
    List<Person> list1 = new ArrayList<>();
    list1.add(new Person());
    List<User> list2 = new ArrayList<>();
    list2.add(new User());

    System.out.println("list1 = " + list1.getClass());
    System.out.println("list2 = " + list2.getClass());
    System.out.println(list1.getClass() == list2.getClass());
  }
}

运行结果:

显然,List<Person> 和 List<User> 在虚拟机中指向的类都是 ArrayList ,泛型信息被擦除了。

在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T> 则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>则类型参数就被替换成类型上限 String。

类型擦除带来的局限性:

类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样理解类型擦除也能让我们绕过泛型本身的一些限制。

正常情况下,因为泛型的限制,编译器不让最后一行代码编译通过,因为类似不匹配。但是,基于对类型擦除的了解,利用反射,我们可以绕过这个限制。

public interface List<E> extends Collection<E>{
    
     boolean add(E e);
}

上面是 List 和其中的 add() 方法的源码定义。

因为 E 代表任意的类型,所以类型擦除时,add 方法其实等同于:

boolean add(Object obj);

那么,利用反射,我们绕过编译器去调用 add 方法

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

    List<Integer> list = new ArrayList<>();
    list.add(123);

    try {
      Method method = list.getClass().getDeclaredMethod("add", Object.class);
      method.invoke(list,"abc");
      method.invoke(list,55.5f);

    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    } catch (InvocationTargetException e) {
      e.printStackTrace();
    }

    for (Object obj : list) {
      System.out.println(obj);
    }
  }
}

运行结果是:

可以看到,根据类型擦除的原理,使用反射的手段就绕过了正常开发中编译器不允许的操作限制。

注意:

  • 泛型类或泛型方法中,不接受8中基本数据类型;
  • 需要使用他们的包装类;

posted on 2019-09-14 22:40  破解孤独  阅读(299)  评论(0编辑  收藏  举报

导航