java里程碑之泛型--类型通配符

时间:2021-01-14 00:09:17

  • 1,泛型与数组在子类上面的对比

在整理java泛型的类型通配符之前,我们先来研究下java在数组设计上的一个不合理。我们来看下面代码:

public class Test
{ public static void main(String[] args)
{
//1。定义一个Integer数组
Integer[] intArray = new Integer[2];
//2。定义一个Number数组,将上面的Integer数组赋值给Number数组
Number[] numArray = intArray;
//3。给现在的数组赋值一个double类型
numArray[0] = 1.0;
//4。检查现在数组的第一个数据
System.out.println(numArray[0]);
//Exception in thread "main" java.lang.ArrayStoreException: java.lang.Double
} }

在数组中,程序可以直接把一个integer[]数组赋值给一个number[]变量。如果试图把一个Double对象保存到该Number[]数组中,编译可以通过,但是在运行时会抛出ArrayStoreException异常的。正如所有编程语言一样,一门设计优秀的语言不仅需要提供强大的功能,而且应该提供强大的错误提示和出错警告,这样子才能尽量避免开发者犯错。但是java允许将Integer[]数组赋值给Number[]变量这显然是一种不安全的设计。



也正是因为上面有可能出现的缺陷,java在设计泛型的时候就进行了改进,java规定:不可以把List<Integer>对象赋值给List<Number>变量。比如说下面的代码将会报错,编译就会报错:

List<Integer> intList = new ArrayList<>();
//Type mismatch: cannot convert from List<Integer> to List<Number>
List<Number> numList = intList;

其实,java泛型的设计原则就是只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。

上面2者对比:如果A是B的一个子类型,包括子类或者子接口,那么A[]依然是B[]的子类型,但是G<A>不是G<B>的子类型。

  • 2,使用类型通配符

OK,那现在我们假设一种情景,我们需要定义一个方法,该方法里面有一个集合形参,集合形参的元素类型是不确定的,那我们应该要怎么写呢?

现在我们来写一段最普通的便利List集合的代码:

public void test(List list)
{
for (int i = 0; i < list.size(); i++)
{
System.out.println(list.get(i));
}
}

上面的代码很简单,但是有一个问题就是说我们在使用上面的list的时候没有传入实际的类型参数,这将引起泛型警告。其实我们也可以理解现在的泛型T就相当于Object类型,这样子可以解决实际的类型参数警告问题,但是我们在调用这个方法的时候传入的实际参数值可能不是我们所期望的,比如下面的调用代码就会报错:

public void test(List<Object> list)
{
for (int i = 0; i < list.size(); i++)
{
System.out.println(list.get(i));
}
} public static void main(String[] args)
{
Test test = new Test();
List<String> list = new ArrayList<>();
test.test(list);
}

上面的代码编译时候就不通过,报错说The method test(List<Object>) in the type Test is not applicable for the arguments (List<String>),也就是说List<String>对象不能被当成List<Object>对象来使用,我们前面说过了,List<String>类并不是List<Object>类的子类。这个时候我们应该要怎么办呢?使用类型通配符。



类型通配符就是一个问号,将一个问号作为类型实参传给list集合,写作List<?>就可以。这种语法的意思是:List<?>是一个元素类型未知的list,然后它的元素类型可以匹配任何类型。我们修改上面的代码:

public void test(List<?> list)
{
for (int i = 0; i < list.size(); i++)
{
System.out.println(list.get(i));
}
} public static void main(String[] args)
{
Test test = new Test();
List<String> list = new ArrayList<>();
test.test(list);
}

OK,现在没有问题了,编译和运行都可以通过。

  • 关于使用类型通配符有2点注意:

1),这种带通配符的list仅仅表示它是各种泛型List的父类,但是并不能将元素添加在其中,除了null之外。其实这点也很好理解的,因为程序不能确定集合里面的元素的类型是什么,当然不能想其中添加对象。

2),程序可以调用get()方法来获得指定索引处的元素,但是其返回值是一个未知类型,其实也就是Object类型。



  • 3,设定类型通配符的上限

我们前面已经代码演示了我们的情景,使用List<?>可以表示任何泛型list的父类。但是假如我们现在要用到更加详细的一种情况呢,比如说我不希望这个List<?>是用来表示任何泛型list的父类,只是希望它代表某一类泛型list的父类。我们使用通配符问号来表示某一类,但是因为我们没有加类型通配符的上限的限制,我们不可避免的在使用父类的方法时候还要做强转,将原来的Object类向下强转到我们的父类,然后才能调用我们定义好的父类的方法。为了满足这种需求,java泛型提供了被限制的泛型通配符。这里说的具体的被限制分为上限和下限2种情况,我们先来看上限,语法是List<?
extends 父类>具体的看下面的代码演示:

package com.linkin.maven.mavenTest;

import java.util.ArrayList;
import java.util.List; public class Test
{ //这个方法太土了,使用了泛型但是要强转
public void test1(List<?> list)
{
for (Object obj : list)
{
if (obj instanceof A)
{
A a = (A) obj;
a.test();
}
}
} public void test(List<? extends A> list)
{
//list.add(new B());此时list代表的类型是未知的,当然不能往里面添加元素了
for (A a : list)
{
a.test();
}
} public static void main(String[] args)
{
Test test = new Test();
List<A> list = new ArrayList<>();
list.add(new B());
list.add(new C());
test.test(list);
//子类B实现抽象类A的方法。。。
//子类C实现抽象类A的方法。。。
} } abstract class A
{
abstract void test();
} class B extends A
{ @Override
void test()
{
System.out.println("子类B实现抽象类A的方法。。。");
} } class C extends A
{ @Override
void test()
{
System.out.println("子类C实现抽象类A的方法。。。");
} }

上面的代码中,List<? extends A>是受限制通配符的一个例子,此处的问号代表一个未知的类型,就想前面我们看到的通配符一样。但是此处的未知类型一定是A的子类,或者是A本身。同样的因为我们无法确定这个类型是什么,我们也不能将任何对象添加到这个集合中。

  • 4,设定类型形参的上限

java泛型不仅可以在使用通配符形参时设定上限,而且还可以在定义类型形参的时候设定上限,用于表示传给该类型参数的实际类型要不就是该上限类型,要不就是该上限类型的子类。具体请看下面代码:

package com.linkin.maven.mavenTest;

import java.util.ArrayList;
import java.util.List; public class Test<T extends A>
{ public static void main(String[] args)
{
Test<A> test1 = new Test<>();
Test<B> test2 = new Test<>();
//下面代码报错。
Test<Integer> test3 = new Test<>();
} } abstract class A
{
abstract void test();
} class B extends A
{ @Override
void test()
{
System.out.println("子类B实现抽象类A的方法。。。");
} } class C extends A
{ @Override
void test()
{
System.out.println("子类C实现抽象类A的方法。。。");
} }

在实际编码过程中,还有一种情况就是说我们为类型形参设定多个上限,至多有一个父类上限,但是可以有多个接口上限,表明该类型形参必须是其父类的子类,并且要实现多个上限接口。具体的语法是:public class Test<T extends A & Serializable>

  • 5,设定类型通配符的下限

如果java仅仅提供类型通配符的上限的话,会在操作某些特定的情景时有缺陷的。完美的东西都是对应的,所以有上限也肯定有下限。现在我们有下面情景:

我们自己要实现一个工具方法,实现将src集合里的元素复制到dest集合里的功能。因为dest集合可以保存src集合里面的所有元素,所以dest集合里面元素的类型应该是src集合里面元素类型的父类。现在我们只使用上限来实现下上面的功能:

public static <T> void copy(List<? extends T> src, List<T> dest)
{
for (T t : src)
{
dest.add(t);
} }

OK,功能实现了。我们在改下情景,现在我们假设该方法需要一个返回值,返回最后一个被复制的元素。代码修改如下:

public static <T> T copy(List<? extends T> src, List<T> dest)
{
T lastt = null;
for (T t : src)
{
dest.add(t);
lastt = t;
}
return lastt;
}

表面上看起来上面的方法实现了这个功能,但是我们仔细研究下就会发现一个问题。我们上面代码复制过程中我们进行复制的元素的类型其实是T的子类,但是最后我们得到的这个元素的类型变成了T,也就是说程序在复制元素的过程中,丢失了src集合元素的类型。代码如下:

package com.linkin.maven.mavenTest;

import java.util.ArrayList;
import java.util.List; public class Test
{
public static <T> T copy(List<? extends T> src, List<T> dest)
{
T lastt = null;
for (T t : src)
{
dest.add(t);
lastt = t;
}
System.out.println(lastt.getClass());
return lastt;
} public static void main(String[] args)
{
List<Integer> src = new ArrayList<>();
src.add(1);
List<Number> dest = new ArrayList<>();
Number number = Test.copy(src, dest);
//虽然实际类型都是Integer,但是这里还是需要强转的
Integer number1 = (Integer) Test.copy(src, dest);
} }

OK,那我们现在使用泛型通配符的下限,就可以很好的绕开这个问题。设定通配符的下限语法是:<? super type>,这个通配符表示它必须是Type本身,或是Type的父类。我们修改前面的方法,不会在发生丢失类型的这种情况了呢。

public class Test
{
public static <T> T copy(List<T> src, List<? super T> dest)
{
T lastt = null;
for (T t : src)
{
dest.add(t);
lastt = t;
}
System.out.println(lastt.getClass());
return lastt;
} public static void main(String[] args)
{
List<Integer> src = new ArrayList<>();
src.add(1);
List<Number> dest = new ArrayList<>();
Integer number1 = Test.copy(src, dest);
} }

关于这一点,我们可以通过使用这种通配符下限的方式定义某个类构造器的参数,就可以将所有可用的参数,或者参数的父类传入,增强了程序的灵活性。比如JDK中TreeSet和TreeMap都有这种用法。具体代码如下:

 public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
public interface Comparator<T> {}

下面是具体的使用代码:

public static void main(String[] args)
{
TreeSet<String> ts1 = new TreeSet<>(new Comparator<Object>()
{
@Override
public int compare(Object o1, Object o2)
{
return 0;
}
}); TreeSet<String> ts2 = new TreeSet<>(new Comparator<String>()
{
@Override
public int compare(String o1, String o2)
{
return 0;
}
}); TreeSet<Integer> ts3 = new TreeSet<>(new Comparator<Integer>()
{
@Override
public int compare(Integer o1, Integer o2)
{
return 0;
}
});
}

上面的代码使用这种带下限的通配符的语法,可以在创建TreeSet对象时候灵活的选择合适的Comparator,编程很灵活也很方便。