Java泛型基础

357 阅读9分钟

一.什么是泛型(What)

泛型就是参数化类型,即我们在定义的时候,将具体的类型进行参数化,在调用或者使用的时候,再传入具体的参数类型,我们可以将泛型用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

二.为什么使用泛型(Why)

泛型的好处:

  1. 适用于多种数据类型执行相同的代码
  2. 类型安全:我们使用泛型的时候,指定了特定的参数类型,这样对其类型进行限定,可以在编译期间对我们传入的参数类型进行判断,增加了类型的安全性
  3. 取消强制类型转换:我们指定明确的类型参数后,由于在编译阶段就会对类型进行约束,泛型会自动且隐式的给我们做类型转换,转换成我们指定的类型,我们不再需要关心类型的转换

三.泛型的使用(How)

泛型可以定义在类、接口、方法上,分别被称为泛型类、泛型接口、泛型方法。

3.1、泛型类

通过 <> 将类型变量T(大写字母都可以,不过常用的就是T,E,K,V等等)括起来,放在类名后面,泛型类可以有多个类型变量。

一个类型变量的泛型类:

/**
*desc:一个类型变量的泛型类
**/
public class NormalGeneric<T>{
    private T data;
    
    public NormalGeneric(){
        
    }
    
    public T getData(){
        return data;
    }
    public void setData(T data) {
        this.data=data;
    }
}

多个类型变量的泛型类:

/**
* 多个类型变量的泛型类
**/
public class NormalGeneric<T,K>{
    private T data;
    private K result;
    
    public NormalGeneric(T t,K k){
        this.data = t;
        this.result = k;
    }
    
    public T getData(){
        return data;
    }
    
    public void setData(T data){
        this.data = data;
    }
    
    public T getResult(){
        return result;
    }
    
    public void setReuslt(K result){
        this.result = result;
    }
}

3.2、泛型接口

泛型接口与泛型类的定义基本相同,在泛型接口名称后面加上<>并指定泛型类型

/**
* 泛型接口
**/
public interface Genertor<T>{
    T getData();
    void setData(T data);
}

我们在实现泛型接口可以使用下面两种方式:

1.未传入泛型实参

在 new 出类的实例时,需要指定具体类型:

public ImplGenerator<T> implements Genertor<T>{
     @Override
    T getData(){
        return null;
    }
    
    @Override
    void setData(T data){
        
    }
}
//使用的时候需要指定具体类型
ImplGenerator<String> i = new ImplGenerator<String>("Impl");

2.传入泛型实参

在 new 出类的实例时,和普通的类没区别。

public ImplGenerator implements Genertor<String>{
     @Override
    String getData(){
        return "";
    }
    
    @Override
    void setData(String data){
        
    }
}
//使用的时候传入对应类型即可
ImplGenerator i = new ImplGenerator("Impl");

3.3、泛型方法

泛型方法,是在调用方法的时候指明泛型的具体类型 ,泛型方法可以在任何地方和任何场景中使用,包括普通类和泛型类。注意泛型类中定义的普通方法和泛型方法的区别。

泛型类中定义的普通方法和泛型方法的区别

泛型类中的普通方法:

// 虽然在方法中使用了泛型,但是这并不是一个泛型方法。
// 这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
// 所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
    return key;
}

泛型方法:

/**
 * 这才是一个真正的泛型方法。
 * 首先在 public 与返回值之间的 <T> 必不可少,这表明这是一个泛型方法,并且声明了一个泛型 T
 * 这个 T 可以出现在这个泛型方法的任意位置,泛型的数量也可以为任意多个。
 */
public <T,K> K showKeyName(Generic<T> container){
    // ...
}

3.4 字母规范

在定义泛型时,指定泛型的变量可以是任意一个大写字母,意义完全相同;但为了提高可读性,通常使用有意义的字母:

  • E- Element,常用在java集合中,如:List,Iterator,Set
  • K,V-Key,Value,代表Map的键值对
  • N-Number,数字
  • T-Type,类型,如String,Integer等

四.泛型类型变量的限制(限定类型变量)

public class ComparableNum<T extends Comparable>{
    
}

public class ComparableList<T extends ArrayList&Comparable>{
    
}
  • 「T表示应该绑定类型的子类型,Comparable表示绑定类型,子类型和绑定类型可以使类也可以是接口」。
  • 「extends 左右都允许多个,如T,V extends Comparable&Serializable」。
  • 「限定类型变量既可以用在泛型方法上也可以用在泛型类上」。

五.泛型通配符

  • ? extends X:表示类型的上界,类型参数是X的子类。
  • ? super X: 表示类型的下界,类型参数是X的超类。

?extends X

如果其中提供了 get 和 set类型参数变量的方法的话,set方法是不允许被调用的,会出现编译错误,而 get 方法则没问题。

?extends X 表示类型的上界,类型参数是 X 的子类,那么可以肯定的说,get方法返回的一定是个 X(不管是 X 或者 X 的子类)编译器是可以确定知道的。但是set方法只知道传入的是个X,至于具体是 X 的哪个子类,是不知道的。

因此,? extends X主要用于安全地访问数据,可以访问X及其子类型,并且不能写入非null的数据。

? super X

如果其中提供了 get 和 set类型参数变量的方法的话,set方法可以被调用,且能传入的参数只能是 X 或者 X 的子类。而 get 方法只会返回一个 Object 类型的值。

? super X 表示类型的下界,类型参数是 X 的超类(包括 X 本身),那么可以肯定的说,get 方法返回的一定是个 X 的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以 get 方法返回Object。编译器是可以确定知道的。对于set方法来说,编译器不知道它需要的确切类型,但是 X 和X 的子类可以安全的转型为 X。

因此,?super X主要用于安全地写入数据,可以写入X及其子类型。

无限定的通配符?

表示对类型没有任何限制,可以把?看成所有类型的父类,如ArrayList<?>。

泛型中的约束和局限性

  • 1、不能用基本类型实例化类型参数。
  • 2、运行时类型查询只适用于原始类型。
  • 3、泛型类的静态上下文中类型变量失效:不能在静态域或方法中引用类型变量。因为泛型是要在对象创建的时候才知道是什么类型的,而对象创建的代码执行先后顺序是 static 的部分,然后才是构造函数等等。所以在对象初始化之前 static 的部分已经执行了,如果你在静态部分引用泛型,那么毫无疑问虚拟机根本不知道是什么东西,因为这个时候类还没有初始化。
  • 4、不能创建参数化类型的数组,但是可以定义参数化类型的数组。
  • 5、不能实例化类型变量。
  • 6、不能使用 try-catch 捕获泛型类的实例。

泛型类型的继承规则

泛型类可以继承或者扩展其它泛型类,比如List和ArrayList:

public static class ExtendsPair<T> extends Pair<T>{
    
}

六.自定义泛型

返回值中存在泛型

public static <T> List<T> parseArray(String response,Class<T> object){
    List<T> modelList = JSON.parseArray(response,object);
    return modelList;
}

使用Class传递泛型类Class对象

我们用Class object来传递类的class对象,因为Class也是一泛型,它是用来装载类的class对象的,它的定义如下:

public final class Class<T> implements Serializable{
    
}

定义泛型数组

public static <T> T[] fun(T...arg){
    return arg;
}

七.泛型的擦除

Java 语言使用类型擦除机制实现了泛型,类型擦除机制,如下:

  • 编译器会把所有的类型参数替换为其边界(上下限)或 Object,因此,编译出的字节码中只包含普通类、接口和方法。
  • 在必要时插入类型转换,已保持类型安全
  • 生成桥接方法以在扩展泛型类时保持多态性

1.泛型类型的擦除

Java 编译器在擦除过程中,会擦除所有类型参数,如果类型参数是有界的,则替换为第一个边界,如果是无界的,则替换为 Object。 例如,我们定义一个无界泛型类如下:

public class Node<T>{
	private T data;
	private Node<T> next;
	public Node(T data,Node<T> next){
		this.data = data;
		this.next = next;
	}
	public T getData(){
		return data;
	}
}

由于类型参数T是无界的,Java编译器会将其替换为Object,如下:

    public class Node{
        private Object data;
        private Node next;
        public Node(Object data,Node next){
            this.data = data;
            this.next = next;
        }
        public Object getData(){
            return data;
        }
    }

我们在定义一个有界泛型类如下:

 public class ComparableNum<T extends Comparable>{
    private T data;	
	private ComparableNum<T> next;
	public ComparableNum(T data,ComparableNum<T> next){
		this.data = data;
		this.next = next;
	}
	public T getData(){
		return data;
	}
 }

Java编译器会将其替换为第一个边界Comparable,如下:

  public class ComparableNum{
      private Comparable data;
      private ComparableNum next;
      public ComparableNum(Comparable data, ComparableNum next) {
          this.data = data;
          this.next = next;
      }
      public Comparable getData() {
          return data; 
      }
  }

2.泛型方法的擦除

Java 编译器同样会擦除泛型方法中的类型参数,例如:

public static <T> int index(T[] array){
	int index = 0;
	...
	return index;
}

由于T是无界的,因此Java编译器将其替换为Object,如下:

public static int index(Object[] array){
	int index = 0;
	...
	return index;
}

同样,有界泛型方法:

public static<T extends Shape> void draw(T shape){
	  ...
}

编译器会将其替换为Shape,如下:

public static void draw(Shape shape){
	  ...
}

3.桥接方法

为了解决泛型擦除后继承类型的多态性,Java编译器会生成一个桥接方法,如下:

public class Node<T>{
	public T data;
	public Node(T data){
		this.data = data;
	}

	public void setData(T data){
		this.data = data;
	}
}

public class ChildNode extends Node<Integer>{
	public ChildeNode(Integer data){
		super(data);
	}
	
	public void setData(Integer data){
		super.setData(data);
	}
}

Java编译器会在子类中生成一个桥接方法,保留泛型类型的多态性,ChildNode中的setData(Object data)和Node中的setData(Object data)可以完成方法覆盖

public class ChildNode extends Node{
	//生成的桥接方法
	public void setData(Object data){
		setData((Integer)data);
	}

	public void setData(Integer data){
		super.setData(data);
	}
}

八.什么时候使用泛型

当接口、类及方法中操作的引用数据类型不确定的时候,以前用Object来进行扩展的,现在可以使用泛型来表示。这样可以避免强转的麻烦,而且将运行问题转移到编译时期。

九.Java类库中的泛型有哪些

所有的标准集合接口都是泛型化的—— Collection、List、Set 和 Map<K,V>。类似地,集合接口的实现都是用相同类型参数泛型化的,所以HashMap<K,V> 实现 Map<K,V> 等。

除了集合类之外,Java 类库中还有几个其他的类也充当值的容器。这些类包括 WeakReference、SoftReference 和 ThreadLocal。