2460 2017-06-10 2020-06-25
前言:对于Java容器载体来说,容器要具有通用性,即不能限制为某个类型的容器。这时,泛型出现了。
一、泛型
1、概述
泛型的出现似乎就是为了完善容器类库的,我们来看下代码
- 最原始的容器类,持有确定类对象
public class Holder1() {
private Test test;
public Holder1(Test test) {
this.test = test;
}
public Test getTest() {
return test;
}
}
- 向上转型的容器类,强制转为目标类型
public class Holder2() {
private Object obj;
public Holder2(Object obj) {
this.obj = obj;
}
public Object getObj() {
return obj;
}
public static void main(String[] hk) {
String str = new String("hk");
Holder2 holder2 = new Holder2(str);
String a = (String)holder2.getObj();
//Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Integer b = (Integer)holder2.getObj();
}
}
- 下面就是泛型了,知道确切对象类型
//当然,也可以这么用:Holder2<A, b, CD, HK>
public class Holder2<HK> {
private HK obj;
public Holder2(HK obj) {
this.obj = obj;
}
public Object getObj() {
return obj;
}
public static void main(String[] hk) {
String str = new String("hk");
Holder2<String> holder2 = new Holder2(str);
System.out.println(holder2.getObj());
}
}
通过上面的代码,泛型的优点就稍稍突显出来了,我们往下看。
2、泛型方法
没错,不仅可以在类结构层次添加泛型,方法层次也支持泛型
- 无返回类型
public <A, B> void get1(A a, B b) {
System.out.println(a.getClass().getName());
}
- 有返回类型,这种情况下需要一个容器载体,因为泛型不能单独存在
public <A, B> void get1(A a, B b) {
return new HashMap<A, B>();
}
二、类型擦除
1、擦除
我们看下下面的两段代码
- 第一段
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
//说明无法从泛型内部发现任何有关泛型参数的类型信息
System.out.println(c1 == c2);//true
- 第二段
class HasF {
public void f() {
System.out.println("HasF.f()");
}
}
class Manipulator<T> {
private T obj;
public Manipulator(T t) {
obj = t;
}
public void manipulate() {
//说明原类型被擦除,即无法从泛型内部发现任何有关泛型参数的类型信息
//编译不通过 obj.f();
}
}
public class Manipulation {
public static void main(String hk) {
HasF hasF = new HasF();
Manipulator<HasF> manipulator = new Manipulator<>(hasF);
manipulator.manipulate();
}
}
通过上面的两段代码,我们知道,被声明泛型的参数的容器,再未明确指定泛型类型前,是无法获取任何有关泛型参数的类型信息,这是因为在JVM运行时把泛型向上转为了Object(List< E >则被转换为原生的List),而当明确指定泛型参数的类型后,JVM再把Object类向下转为目标参数类型,如下
//new Manipulator<>(hasF)在编译时,等同于new Manipulator<Object>(hasF),前面的代码代表了向下转换的指令
Manipulator<HasF> manipulator = new Manipulator<>(hasF);
2、擦除补偿
擦除机制显然是存在问题的,但也可以在代码层次进行补偿,或者说防止跳坑。
- Sun建议的显式工厂方式,代码如下
public class FactoryConstraint {
public static void main(String[] hk) {
new Foo2<Integer>(new IntegerFactory());
new Foo2<Widget>(new Widget.Factory());
}
}
interface FactoryI<T> {
T create();
}
class Foo2<T> {
private T x;
public <F extends FactoryI<T>> Foo2(F factory) {
x = factory.create();
}
}
class IntegerFactory implements FactoryI<Integer> {
public Integer create() {
return new Integer(0);
}
}
class Widget {
public static class Factory implements FactoryI<Widget> {
public Widget create() {
return new Widget();
}
}
}
通过这种方式可以获得编译时期的类型检查,下同。
- 另外一种是模板方法,代码如下
public class CreatorGeneric {
public static void main(String[] hk) {
Creator c = new Creator();
c.f();
}
}
abstract class GenericWithCreate<T> {
final T element;
GenericWithCreate() {
element = create();
}
abstract T create();
}
class X{}
class Creator extends GenericWithCreate<X> {
X create() {
return new X();
}
void f() {
System.out.println(element.getClass().getSimpleName());
}
}
小结:上面两种都是通过设计模式来对Java泛型的类型擦除进行补救,对于具体的设计模式,这里不做深究,了解就行了。
三、泛型数组
1、概述
我们先看下下面的代码,如下
- 第一段
class<T[]> Test{}//错误语法,泛型不能定义为数组
- 第二段
class TestClass<T>{}
public class Test {
public static void main(String[] hk) {
// TestClass<String>[] test = new TestClass<String>[1];//语法错误,不能创建泛型数组
TestClass<String>[] test1 = new TestClass[1];//语法通过,可能有点不理解,往下看
System.out.println(test1.length);//1
test1[0] = new TestClass<String>();
System.out.println(test1[0] == null);//false,说明泛型参数不起作用,本质还是TestClass数组
ArrayList<String>[] ss = new ArrayList[10];
System.out.println(ss.length);//为10
}
}
通过上面我们知道了,Java里面是不支持泛型数组的实例化(虽然可以定义)。问题在于数组将跟踪它们的实际类型,而这个类型是在数组被创建时确定的。因此,上面的所谓泛型数组,将忽略泛型参数,从而变为数组。成功创建泛型数组的唯一方式,不是实例化一个泛型数组,而是先创建一个被擦除类型的新数组,再对其转型。如下
- 第三段
class Generic<T>{}
public class ArrayOfGeneric {
static final int SIZE = 100;
static Generic<Integer>[] gia;
public static void main(String[] args) {
//gia = (Generic<Integer>[])new Object[SIZE];运行抛出异常
gia = (Generic<Integer>[]) new Generic[SIZE];
System.out.println(gia.getClass().getSimpleName());
gia[0] = new Generic<Integer>();
//gia[0] = new Generic<Object>();编译不通过
//gia[1] = new Object();编译不通过
//gia[2] = new Generic<Double>();编译不通过
//Double d = (Double)gia[1];编译不通过
//Object dd = (Object)gia[1];编译通过
}
}
在Java中,数组会追踪数组对象的实际类型,这意味着数组内部的元素不能通过任何方式的转型进入进行(Object类也不行),但可以转型转出去。
2、T[] 数组
我们看一下稍稍复杂一点的代码
public class GenericArray<T> {
private T[] array;
public GenericArray(int size) {
array = (T[]) new Object[size];
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] rep() {
return array;
}
public static void main(String[] args) {
GenericArray<Integer> gai = new GenericArray<>(10);
Object[] oa = gai.rep();
//ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer
//Integer[] ia = gai.rep();
}
}
常规来讲rep返回的就是Integer[]数组,但结果却不是。这是因为擦除机制,即泛型数组的运行时类型只能是Object[]数组,不能向下转。如果我们立即将其转型为T[]数组,如Integer[] ia = gai.rep()
那么在编译期该数组的实际类型就将丢失,而编译器可能会错过某些潜在的错误检查。正因为这样,最好在集合内部使用Object[]数组,取出使用时,再将对Object[]转为T[]。
3、类型标记
这是一种新的方式,稍稍改了一下上面的代码,我们看下
public class GenericArrayWithTypeToken<T> {
private T[] array;
public GenericArrayWithTypeToken(Class<T> type, int size) {
//类型标记Class<T>被传递到构造器中,以便从擦除中恢复,使得我们可以得到目标数组
array = (T[]) Array.newInstance(type, size);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] rep() {
return array;
}
public static void main(String[] hk) {
GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<>(Integer.class, 10);
Integer[] ia = gai.rep();//运行正常
}
}
说了这么多,自己稍稍总结下:忘了有泛型数组这回事,转而对Object[]数组进行转型,感觉就差不多了。
四、边界
这个很好理解,是对泛型参数的一个类型限制,用于限定特定的泛型(以便于调用父泛型中的方法),如下
class AA{}
interface BB{
void bb();
}
class AB extends AA implements BB {
@Override
public void bb() {
System.out.println("ab");
}
}
class CC<T extends AA>{}//这里的extends不区分父类和接口
class DD<T extends BB>{}
class EE<T extends AA & BB>{}
//class FF<T extends BB & AA>{}语法错误,&后面只能接接口,而且可以接多个,和implements类似
class GG<T extends EE>{}
public class TestB{
public static void main(String[] args) {
//CC<BB> ccc = new CC<BB>();编译不通过
CC<AA> cc = new CC<AA>();
CC<AB> ccc = new CC<AB>();
EE<AB> cccc = new EE<AB>();
}
}
小结下:
- extends不区分父类和接口
- &后面只能接接口,且可以接多个
- 泛型类型的限制和普通类的限制相似,单继承,多接口
五、通配符
1、协变、逆变
这个了解概念即可,不做深究,代码如下
- 协变:用窄类型替代宽类型
public class Test {
public static void main(String[] args) {
Number num1 = new Integer(0);//Integer extends Number
Number[] num2 = new Integer[10];//说明数组是协变的
ArrayList<Number> flist = new ArrayList<Integer>();//不能编译,旨在说明泛型不是协变的
}
}
- 逆变:用宽类型覆盖窄类型
在Java中,不允许把子类赋给父类对象,但可以通过通配符模拟实现,见下文。
2、子类型通配符
先看代码
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class GenericsAndCovariance {
public static void main(String[] args) {
// 标识为一个Fruit的List,至于具体是什么,并不关心
List<? extends Fruit> flist = new ArrayList<Apple>();
// 下面的都不能编译通过
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());
flist.add(null); // null是可以的
// 我们可以确定里面的必定是Fruit类型
Fruit f = flist.get(0);
}
}
那么问题来了,我这个List<? extends Fruit>
什么都不能做,连一个Apple对象都添加不进去,是这样的。它唯一的功能是指向一个继承Fruit类的List,比如指向List<Orange>
,并且我们是可以Fruit对象的方法,而Orange对象的方法则被丢失了。
3、父类型通配符
先看代码
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // 不能添加
}
}
上面这段代码是可行的。有了上面讲得两个通配符后,我们可以对泛型进行更好的读写操作,如下
public class Collections {
//数据源是src,里面存储的肯定是T类型或T的子类,目标是dest,里面存储的是T类型或T的子类
//一个只能读,且读出来的肯定能写进另一个
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++)
dest.set(i, src.get(i));
}
//完美,不是吗?
}
4、无界通配符
不讲了,看代码
ArrayList<?> list = new ArrayList<String>();
//编译不通过,即不能对list进行add操作
//list.add(new String("123"));
System.out.println(list.size());//0
ArrayList<String> list1 = new ArrayList<>();
list1.add("123");
list = list1;//指向List引用
System.out.println(list.get(0));//123
System.out.println(list1.get(0));//123
总访问次数: 146次, 一般般帅 创建于 2017-06-10, 最后更新于 2020-06-25
欢迎关注微信公众号,第一时间掌握最新动态!