泛型简介
泛型作为jdk1.5
进入的技术,避免我们在操作集合时获取元素进行强转操作,以及其他类型元素误插入的问题。甚至他使得我们提高我们类的通用性,具体我们会在后文展开详尽介绍。
泛型基础使用示例
实现一个泛型接口
接口定义,可以看到我们只需在接口上增加泛型声明<T>
即可
package com.shark.wiki.interview.javaBase.Generator;
/**
* 泛型接口
* @param <T>
*/
public interface GeneratorInterface<T> {
T getVal();
}
实现类,可以看到我们的实现类同样可以不指定具体类型
/**
* 实现泛型接口不指定类型
* @param <T>
*/
public class GeneratorImpl<T> implements GeneratorInterface<T> {
@Override
public T getVal() {
return null;
}
}
指定类型的泛型接口继承类示例
package com.shark.wiki.interview.javaBase.Generator;
/**
* 泛型接口指定类型
*/
public class GeneratorImpl2 implements GeneratorInterface<String> {
@Override
public String getVal() {
return null;
}
}
泛型方法示例
声明泛型方法的方式很简单,只需在返回类型前面增加一个<E>
即可
package com.shark.wiki.interview.javaBase.Generator;
import org.omg.PortableServer.LIFESPAN_POLICY_ID;
import java.util.ArrayList;
import java.util.List;
/**
* 泛型方法
*/
public class GeneratorMethod {
public static <E> void printArray(List<E> array){
for (E e : array) {
System.out.println(e);
}
}
public static void main(String[] args) {
List<String> list=new ArrayList<>();
list.add("11");
list.add("11");
list.add("11");
list.add("11");
list.add("11");
list.add("11");
GeneratorMethod.printArray(list);
}
}
泛型类使用示例
与泛型接口用法差不多,在类名后面增加<T>
即可。
/**
* 泛型类的用法
* @param <T>
*/
public class GenericObj<T> {
private T key;
public T getKey() {
return key;
}
public void setKey(T key) {
this.key = key;
}
public static void main(String[] args) {
GenericObj<Integer> obj=new GenericObj();
obj.setKey(1);
}
}
泛型的使用场景
泛型大部分是应用于项目开发中通用对象例如我们常用的Map
为什么说Java是一门伪泛型语言呢?
证明1——反射存入非泛型元素
Java本质就一门伪泛型语言,泛型的作用仅仅在编译期间进行类型检查的,一旦生成字节码之后,关于泛型的一切都会消失。
如下所示,Integer
类型数组我们完全可以通过反射将字符串存到列表中。
public static void main(String[] args) throws Exception {
List<Integer> list=new ArrayList<>();
list.add(1);
// list.add("s"); 报错
Class<? extends List> clazz=list.getClass();
// java的泛型时伪泛型,运行时就会被擦除
Method add = clazz.getDeclaredMethod("add", Object.class);
add.invoke(list,"k1");
System.out.println(list);
/**
* 输出结果
* [1, k1]
*/
}
证明2——泛型形参重载失败
设计者将Java
泛型在编译器后擦除的原因还有如下原因:
- 避免引入泛型创建没必要的新类型
- 节约虚拟机开销
这一点我们用如下的例子就能看出,相同参数不通泛型的方法根本不能重载
既然编译器要把泛型擦除,为什么还要用泛型呢?用Object不行嘛?
- 使用泛型后便于集合的取操作,且提高的代码的可读性。
- 如下代码所示,虽然一下代码在编译后会擦除为
Object
类型,但是通过泛型限定后,JVM
就会自动将其强转为Comparable
类型,减少我们编写一些没必要的代码。
public class Test2 {
public static void main(String[] args) {
List<? extends Comparable> list=new ArrayList<>();
for (Comparable comparable : list) {
comparable.compareTo("1");
}
}
}
什么是桥方法
桥方法其实并不是什么高大上的概念,无非是继承泛型类
并指定泛型类型
,IDE
会自动为我们创建构造方法调用父类的有参构造函数确保泛型多态类。
泛型类,指定了一个有参构造函数
public class Node<T> {
public T data;
public Node(T data) {
this.data = data;
}
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
实现类,自动补充构造方法并调用父类构造方法确保实现泛型的多态性。
public class MyNode extends Node<Integer>{
//继承泛型类后自动添加的,用于保证泛型的多态性
public MyNode(Integer data) {
super(data);
}
}
泛型有哪些限制?
泛型不可以被实例化,如下所示
泛型会在编译器擦除,所以泛型在编译器还未知,所以不可被实例化
泛型参数不可以是基本类型
我们都知道泛型仅在编译器存在,当编译结束泛型就会被擦除,对象就会编程Object类型,所以基本类型作为泛型参数ide就会直接报错
泛型无法被实例化,无论是泛型变量还是泛型数组
从上文我们就知道泛型会在编译期完成后被擦除,这正是因为JVM
不想为泛型创建新的类型造成没必要的开销
不能抛出或者捕获T类型的泛型异常
之所以catch使用泛型会编译失败,是因为若引入泛型后,编译器无法直到这个错误是否是后续catch类的父类
不能声明泛型错误
如下图所示,泛型会在编译器被擦除,那么下面这段代码的catch
就等于catch
两个一样的错误,出现执行矛盾。
try{
}catch(Problem<String> p){
}catch(Problem<Object> p){
}
不能声明两个参数一样泛型不同的方法
编译器擦除后,参数一样,所以编译失败
泛型不能被声明为static
泛型只有在类创建时才知晓,而静态变量在类加载无法知晓,故无法通过编译
以下代码是否能编译,为什么?
- 例1
public final class Algorithm {
public static <T> T max(T x, T y) {
return x > y ? x : y;
}
}
答:错误,T类型未知,无法比较,编译失败
- 例2
public class Singleton<T> {
public static T getInstance() {
if (instance == null)
instance = new Singleton<T>();
return instance;
}
private static T instance = null;
}
答案
不能,泛型不能被static修饰
泛型的通配符介绍
什么是通配符,它用于解决什么问题
我们都知道通配符是解决泛型之间无法协变的问题,当我们使用一种类型作为泛型参数时,却无法使用他的父类或者子类进行赋值,而通配符就是解决这种问题的对策。
上界通配符
有时我们不知道子类的具体类型,上界通配符就是用于解决那些父类引用指向子类泛型引用的场景,所以上界通配符的设计增强了代码的通用性。
上界通配符使用示例
定义父类
/**
* 水果父类
*/
public class Fruit {
}
子类代码
/**
* 水果的子类 苹果
*/
public class Apple extends Fruit {
}
容器类代码
/**
* 容器类
* @param <T>
*/
public class Container<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
测试代码,如下所示上界通配符使得苹果类可以作为水果类的指向引用。
/**
* 泛型测试
*/
public class TestParttern {
public static void main(String[] args) {
Container<? extends Fruit> container=new Container<Apple>();
Fruit data = container.getData();
container.setData(new Apple());
}
}
为什么上界通配符只能get不能set
如上代码所示,当我们用上界通配符? extends Fruit
,我们用其子类作为泛型参数,这只能保证我们get
到的都是这个子类的对象。
但我们却忘了一点,当我们用子类apple
作为泛型参数时,泛型的工作机制仅仅是对这个对象加个一个编号CAP#1
,当我set
一个新的对象,编译器无法识别这个对象类型是否和编号匹配。
更通俗的理解,上界通配符决定可以指向的容器,但是真正使用是并不知晓这个容器是哪个子类容器。所以无法set
。
下界通配符
下界通配符使用示例
这里使用的对象还是上述对象,只不过通配符改为下界通配符
/**
* 泛型测试
*/
public class TestParttern {
public static void main(String[] args) {
Container<? super Apple> container1=new Container<Fruit>();
}
}
下界通配符原理介绍
下界通配符决定了泛型的最大粒度的上限,通俗来说只要是苹果类的父亲都可以作为被指向的引用。通过super
声明,它可以很直观的告诉我们泛型参数必须传super后的父类如下所示
Container<? super Apple> container1=new Container<Fruit>();
为什么下界通配符只能set不能get(或者说get的是object)
原因如下:
1. 下界通配符决定泛型的类型上限,所有水果类的父亲都可以作为指向的引用
2. get时无法知晓其具体为哪个父亲,所以取出来的类型只能是object
Container<? super Apple> container1=new Container<Fruit>();
Object data = container1.getData();
如何获取泛型类型
public class GenericType<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static void main(String[] args) {
//注意这个类要使用子类,笔者为了方便期间使用了 {}
GenericType<String> genericType = new GenericType<String>() {};
Type superclass = genericType.getClass().getGenericSuperclass();
//getActualTypeArguments 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]
Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
System.out.println(type);//class java.lang.String
}
}
参考文献