Java 泛型
泛型简介
泛型作为jdk1.5进入的技术,避免我们在操作集合时获取元素进行强转操作,以及其他类型元素误插入的问题。甚至他使得我们提高我们类的通用性,具体我们会在后文展开详尽介绍。
基础示例
泛型接口
接口定义,可以看到我们只需在接口上增加泛型声明即可
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;
}
}
泛型方法
声明泛型方法的方式很简单,只需在返回类型前面增加一个 即可
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);
}
}
泛型类
与泛型接口用法差不多,在类名后面增加即可。
/**
* 泛型类的用法
* @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
什么是泛型擦除,为什么要泛型擦除呢
证明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泛型在编译器后擦除的原因还有如下原因: 1. 避免引入泛型创建没必要的新类型 2. 节约虚拟机开销
这一点我们用如下的例子就能看出,相同参数不通泛型的方法根本不能重载
既然编译器要把泛型擦除,为什么还要用泛型呢?用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");
}
}
}
什么是桥方法
示例
编写一个带有泛型的父类
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
子类继承该类,设置泛型为Date,并重写该方法
public class DateInter extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
测试代码
public static void main(String[] args) {
DateInter d = new DateInter();
d.setValue(new Date());
// d.setValue(new Object());//编译报错,所以setValue被重写了
}
重写的真相,桥方法
我们都知道泛型会在编译时期被擦除,所以我们父类的getValue应该是返回Object类型,而子类所谓"重写"用的却是Date类型。这根本不成立。一个Object类型的setValue,一个Date类型的setValue,这种情况若是我们自己的写的代码编译器都过不了。
所以这一切都在告诉我们泛型方法的重写根本就是幌子,我们通过下面这段命令反编译一下字节码
javap -c DateInter
可以看到下面这段代码,实际上是生成一个Object类型的同方法名的桥方法(如果我们方法名相同返回值不同是不会通过编译的,但是桥方法可以),"重写"的方法调用这些桥方法,使得我们的代码从便面上看重写成功了,这就是所谓的桥方法。
public class com.zsy.onJava8.generic.brige.DateInter extends com.zsy.onJava8.generic.brige.Pair<java.util.Date> {
public com.zsy.onJava8.generic.brige.DateInter();
Code:
0: aload_0
1: invokespecial #1 // Method com/zsy/onJava8/generic/brige/Pair."<init>":()V
4: return
public void setValue(java.util.Date);
Code:
0: aload_0
1: aload_1
2: invokespecial #2 // Method com/zsy/onJava8/generic/brige/Pair.setValue:(Ljava/lang/Object;)V
5: return
public java.util.Date getValue();
Code:
0: aload_0
1: invokespecial #3 // Method com/zsy/onJava8/generic/brige/Pair.getValue:()Ljava/lang/Object;
4: checkcast #4 // class java/util/Date
7: areturn
//下面这两个方法都是jvm为我们生成的桥方法,jvm允许桥方法方法名和"重写"的方法名一样,通过桥方法的调用,完成表象上的重载
public void setValue(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #4 // class java/util/Date
5: invokevirtual #5 // Method setValue:(Ljava/util/Date;)V
8: return
public java.lang.Object getValue();
Code:
0: aload_0
1: invokevirtual #6 // Method getValue:()Ljava/util/Date;
4: areturn
}
泛型有哪些限制?
泛型不可以被实例化,如下所示
泛型会在编译器擦除,所以泛型在编译器还未知,所以不可被实例化
泛型参数不可以是基本类型
我们都知道泛型仅在编译器存在,当编译结束泛型就会被擦除,对象就会编程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();
泛型作为元组返回多对象
场景简介
有时某个函数需要返回多个对象,而return只能返回一个对象,我们当然可以通过创建类的方式解决问题,但如果需要考虑多返回值类型众多,为了通用性我们也建议使用泛型元组解决问题
代码示例
返回两个对象的元组类,可以看到我们的返回对象都用public,有人可能会说这不是就导致安全性问题,实际上仔细查看代码我们使用final关键字解决了这个问题
public class Tuple2<A, B> {
public final A a1;
public final B a2;
public Tuple2(A a, B b) { a1 = a; a2 = b; }
public String rep() { return a1 + ", " + a2; }
@Override
public String toString() {
return "(" + rep() + ")";
}
}
三个返回值类型的元组类
/**
* 基于父类的元组扩展
*
* @param <A>
* @param <B>
* @param <C>
*/
public class Tuple3<A, B, C> extends Tuple2<A, B> {
pulic final C obj3;
public Tuple3(A obj1, B obj2, C obj3) {
super(obj1, obj2);
this.obj3 = obj3;
}
@Override
public void rep() {
System.out.println("obj1:" + obj1 + " obj2:" + obj2 + " obj3:" + obj3);
}
}
使用示例
public static void main(String[] args) {
Tuple2<Integer, String> tuple2 = new Tuple2<Integer, String>(1, "hello");
tuple2.rep();
Tuple3<Integer,String, Automobile> tuple3=new Tuple3<>(1,"hello",new Automobile());
tuple3.rep();
}
使用泛型其他示例
实现一个链式栈类
// generics/LinkedStack.java
// 用链式结构实现的堆栈
public class LinkedStack<T> {
private static class Node<U> {
U item;
Node<U> next;
Node() { item = null; next = null; }
Node(U item, Node<U> next) {
this.item = item;
this.next = next;
}
boolean end() {
return item == null && next == null;
}
}
private Node<T> top = new Node<>(); // 栈顶
public void push(T item) {
top = new Node<>(item, top);
}
public T pop() {
T result = top.item;
if (!top.end()) {
top = top.next;
}
return result;
}
public static void main(String[] args) {
LinkedStack<String> lss = new LinkedStack<>();
for (String s : "Phasers on stun!".split(" ")) {
lss.push(s);
}
String s;
while ((s = lss.pop()) != null) {
System.out.println(s);
}
}
}
继承列表实现泛型随机存取列表类
// generics/RandomList.java
import java.util.*;
import java.util.stream.*;
public class RandomList<T> extends ArrayList<T> {
private Random rand = new Random(47);
public T select() {
return get(rand.nextInt(size()));
}
public static void main(String[] args) {
RandomList<String> rs = new RandomList<>();
Arrays.stream("The quick brown fox jumped over the lazy brown dog".split(" ")).forEach(rs::add);
IntStream.range(0, 11).forEach(i ->
System.out.print(rs.select() + " "));
}
}
如何获取泛型类型
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
}
}
泛型数组相关面试题
泛型数组:能不能采用具体的泛型类型进行初始化?
不能,如下代码所示第一行就报错了,原因很简单,由于泛型擦除机制 导致数组lsa[1]存放的是List元素为object类型,要想向下面代码这样拿到String类必须要强转才行,这就违背了泛型的原则,所以Java不支持泛型数组。
List<String>[] lsa = new List<String>[10]; // Not really allowed.
// 转obj再转obj数组
Object o = lsa;
Object[] oa = (Object[]) o;
// 声明一个list添加元素
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// 存到转来的obj数组中
oa[1] = li; // Unsound, but passes run time store check
/**
* 由于泛型擦除机制 导致数组lsa[1]存放的是List元素为object类型,要想向下面代码这样拿到String类必须要强转才行
*/
String s = lsa[1].get(0); // Run-time error ClassCastException.
所以java仅仅支持通配符数组
//通配符需要做显示强转,所以编译通过
List<?>[] lsa = new List<?>[10];
// 转obj再转obj数组
Object o = lsa;
Object[] oa = (Object[]) o;
// 声明一个list添加元素
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// 存到转来的obj数组中
oa[1] = li; // Unsound, but passes run time store check
Integer integer = (Integer) lsa[1].get(0); // Run-time error ClassCastException.
System.out.println(integer);