一、泛型定义及作用

泛型是一种编程机制,允许在编写代码时使用参数化类型以在编译时实现类型安全。 以下是泛型作用:

  1. 增强代码可读性和可维护性:通过在代码中使用泛型参数,可以使代码更清晰、更具有可读性和可维护性。
  2. 提高代码安全性:泛型可以在编译时检查类型,从而防止在运行时出现类型转换错误。
  3. 增加代码重用性:泛型允许在不同的数据类型上编写通用代码,从而提高代码的重用性。
  4. 简化代码:使用泛型可以避免重复编写类似的代码,从而简化代码。

总之,泛型是一种强大的编程机制,它可以帮助开发者编写更具可读性、可维护性、安全性和重用性的代码。

Java 泛型在编译期间会执行类型擦除(Type Erasure)。类型擦除是指编译器在编译泛型代码时,会将泛型类型擦除为其原始类型或限定类型,从而在运行时不存在泛型类型。在进行类型擦除时,编译器会将泛型类型参数替换为它们的边界(限定)类型(如果有的话),如果没有指定限定类型,则会将泛型类型参数替换为 Object 类型

例如,一个泛型类定义如下:

public class Box<T> {
    private T content;
    public void setContent(T content) {
        this.content = content;
    }
    public T getContent() {
        return content;
    }
}

在编译后,类型擦除会将泛型类型 T 擦除为 Object 类型,生成的字节码如下:

public class Box {
    private Object content;
    public void setContent(Object content) {
        this.content = content;
    }
    public Object getContent() {
        return content;
    }
}

需要注意的是,尽管在运行时不存在泛型类型,但是在编译期间编译器仍会对泛型类型进行类型检查,从而确保类型安全。

二、泛型使用

1、泛型定义在类上

代码如下:

class CommonUtil {

  public Object obj;

  public Object getObj() {
    return obj;
  }

  public void setObj(Object obj) {
    this.obj = obj;
  }
}

在使用的时候,代码如下:

public static void main(String[] args) {

    CommonUtil tool = new CommonUtil<>();
    
    tool.setObj(2);
    Integer val1 = (Integer)tool.getObj();
    
	tool.setObj("hello java");
    String val2 = (String)tool.getObj();
  }

从上述代码可以看出,在获取值时每次都需要强转,稍不留神就容易发生强转错误。所以能不能通过一种方式避免类型强转呢?答案是:泛型。将泛型定义在类上,就可以避免类型转换。 改进代码如下:

class CommonUtil<T> {

  public T obj;

  public T getObj() {
    return obj;
  }

  public void setObj(T obj) {
    this.obj = obj;
  }
}

在创建实例时把具体类型传入,代码如下:

public static void main(String[] args) {

    CommonUtil<Integer> tool = new CommonUtil<>();
    tool.setObj(2);
    Integer val1 = tool.getObj();
    
    CommonUtil<String> tool = new CommonUtil<>();
	tool.setObj("hello java");
    String val2 = tool.getObj();
  }

2、泛型定义方法上

泛型定义在类上有个不太有好的一点,看下面例子,代码如下:

class CommonUtil<T> {

  public void show(T obj){
    System.out.println("obj = " + obj);
  }
}

代码使用代码如下:

CommonUtil<Integer> tool1 = new CommonUtil<>();
    tool1.show(value);

    CommonUtil<String> tool2 = new CommonUtil<>();
    tool2.show("111");
    
	CommonUtil<Person> tool2 = new CommonUtil<>();
    tool2.show(new Person());

发现没,每次调用 show() 方法,都需要在通过创建实例,因为在创建时可以指定具体使用的参数类型。但是这样创建对象太麻烦,怎么解决,可以将泛型定义在方法上,代码如下:

class CommonUtil {

  public <W> void show(W obj){
    System.out.println("obj = " + obj);
  }
}

使用代码如下:

CommonUtil tool1 = new CommonUtil<>();
    tool1.show(value);
    tool1.show("111");
    tool1.show(new Person());

最后在方法调用时传入具体参数类型,这样就可以避免重复创建对象,做到一个方法重复调用,和 Object 非常类似。包括 static 修饰的静态方法也是一样可以使用。代码如下:

class CommonUtil {

  public static <W> void show(W obj){
    System.out.println("obj = " + obj);
  }
}

使用代码如下:

CommonUtil.show(value);
    CommonUtil.show("111");
    CommonUtil.show(new Person());

但是泛型定义在静态方法上还要注意,这个静态方法不能使用类上面的泛型。为什么?因为类是在创建时才会指定具体参数类型,而静态方法是在类实例化之前就已经被加载到 JVM,根本不知道你类在创建实例时传入的是什么具体参数类型。错误示例如下:

class CommonUtil<W> {

  public static <W> void show(W obj){
    System.out.println("obj = " + obj);
  }
}

3、泛型定义在接口上

有时候泛型会定义在接口上,代码如下:

public interface Inter<T> {

	public void print(T obj);
}

接口上的泛型那么可以在子类中指定、或不指定,子类指定具体类型如下:

public InterImpl implements Inter<String> {
	// 接口上的方法
	public void print(String obj){}
	// 子类独有方法
	protect void show(Object obj){}
}

在使用时,代码如下:

Inter<String> inter = new InterImpl();
	inter.print("hello");
	
	InterImpl impl = new InterImpl();
	impl.show(new Object());

或者子类中定义泛型,接口定义具体类型,代码如下:

public InterImpl<W> implements Inter<String> {
	// 接口上的方法
	public void print(String obj){}
	// 子类独有方法
	protect void show(W obj){}
}

在使用时,代码如下:

Inter<String> inter = new InterImpl();
	inter.print("hello");
	
	InterImpl<Integer> impl = new InterImpl();
	impl.show(new Integer(10));

子类中也不知道具体类型,那么也可以定义泛型,把子类泛型传给接口,代码如下:

public InterImpl<W> implements Inter<W> {
	// 接口上的方法
	public void print(W obj){}
	// 子类独有方法
	protect void show(W obj){}
}

在使用时,代码如下:

Inter<String> inter = new InterImpl();
	inter.print("hello");
	
	InterImpl<Integer> impl = new InterImpl();
	impl.show(new Integer(10));

4、泛型通配符

举个例子,代码如下:

public class FanxinDemo {

  public static void print(Collection<String> list) {
    list.forEach(e->{
      System.out.println("e = " + e);
    });
  }

  public static void main(String[] args) {

    List<String> list1 = new ArrayList<>();
    list1.add("a");
    list1.add("b");
    list1.add("c");

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

    print(list1);
 }
}

可以发现 print() 方法只能输出参数类型是 String 的,其他的参数都不能,怎么可以做到公用这段代码?这里介绍回个泛型通配符 ?,当你不知道泛型是什么类型是,就可以用这个暂时表示,代码如下:

public class FanxinDemo {

  public static void print(Collection<?> list) {
    list.forEach(e->{
      System.out.println("e = " + e);
    });
  }

  public static void main(String[] args) {

    List<String> list1 = new ArrayList<>();
    list1.add("a");
    list1.add("b");
    list1.add("c");

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

    print(list1);
    print(list2);
 }
}

但是假设现在不想让 print() 方法被 Integer 类型参数使用,那么需要怎么做呢?这就需要泛型限定了。

5、泛型限定

泛型限定可以限制参数类型范围,? 这个级别范围太大,不安全,通过 extends、super 关键字来限定范围,但是这两个关键字只能是单继承,所以也是有局限的。限定分为两种:让所有父类可以使用,让所有的子类都可以使用

5.1、让所有父类可以使用

代码如下:

public static void print(Collection<? super Integer> list) {
    list.forEach(e->{
      System.out.println("e = " + e);
    });
  }

表示让 Integer 所有父类都可以使用 print() 方法。

5.2、让所有子类可以使用

代码如下:

public static void print(Collection<? extends String>> list) {
    list.forEach(e->{
      System.out.println("e = " + e);
    });
  }

表示让所有的 String 子类可以使用 print() 方法。