- 1,泛型与数组在子类上面的对比
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,使用类型通配符
现在我们来写一段最普通的便利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点注意:
2),程序可以调用get()方法来获得指定索引处的元素,但是其返回值是一个未知类型,其实也就是Object类型。
- 3,设定类型通配符的上限
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,设定类型形参的上限
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,设定类型通配符的下限
我们自己要实现一个工具方法,实现将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,编程很灵活也很方便。