泛型入门:
Java集合有个缺点:把一个对象“丢进”集合里之后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。
Java集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以把集合设计成可以保存任何类型的对象,只要求有很好的通用性。但这样
做带来了两个问题:
1.集合对元素类型没有任何限制,这样引发一些问题。如:想创建一个只能保存Dog对象的集合,但程序也可以轻易的将Cat对象“丢”进去,所以可能引发异常
2.由于把对象“丢进”集合时,集合丢失了对象的状态信息,集合只知道盛装它的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制转换既增加了编
程的复杂度,也可能引发ClassCastException异常。
编译时不检查类型的异常:
1 import java.util.List; 2 import java.util.ArrayList; 3 4 public class ListErr{ 5 public static void main(String[] args){ 6 //创建一个只想保存字符串的List集合 7 List strList = new ArrayList(); 8 strList.add("疯狂Java讲义"); 9 strList.add("疯狂Android讲义"); 10 //"不小心"把一个Integer对象"丢进"了集合 11 strList.add(5); 12 strList.forEach(str -> System.out.println(((String) str).length())); 13 } 14 }
使用泛型
从Java5后,Java引入了”参数化类型“(parameterized type)”概念,允许程序在创建集合时指定集合元素的类型。Java的参数化类型被称为泛型(Generic)
1 import java.util.List; 2 import java.util.ArrayList; 3 4 public class GenericList{ 5 public static void main(String[] args){ 6 //创建一个只想保存字符串的List集合 7 List<String> strList = new ArrayList<String>(); 8 strList.add("疯狂Java讲义"); 9 strList.add("疯狂Android讲义"); 10 //"不小心"把一个Integer对象"丢进"了集合 11 strList.add(5); 12 strList.forEach(str -> System.out.println(((String) str).length())); 13 } 14 }
Java7泛型的“菱形”语法:
在Java7以前,若使用带泛型的接口、类定义变量,那么调用构造器创建对象时构造器后面也必须带泛型,这就显得有些多余:
List<String> strList = new ArrayList<String>();
Map<String, Integer> scores = new HashMap<String, Integer>();
Java8之后,可以这样写:
List<String> strList = new ArrayList<>();
Map<String, Integer> scores = new HashMap<>();
把两个尖括号并排放在一起非常像一个菱形,这种语法也被称为“菱形”语法。
1 import java.util.List; 2 import java.util.ArrayList; 3 import java.util.Map; 4 import java.util.HashMap; 5 6 7 public class DiamondTest{ 8 public static void main(String[] args){ 9 //Java 自动推断出ArrayList的<>里应该是String 10 List<String> books = new ArrayList<>(); 11 books.add("疯狂Java讲义"); 12 books.add("疯狂Android讲义"); 13 //遍历books集合,集合元素就是String类型 14 books.forEach(ele -> System.out.println(ele.length())); 15 //Java自动推断出HashMap的<>里应该是String,List<String> 16 Map<String, List<String>> schoolsInfo = new HashMap<>(); 17 //Java自动推断出ArrayList的<>里应该是String 18 List<String> schools = new ArrayList<>(); 19 schools.add("斜月三星洞"); 20 schools.add("西天取经路"); 21 schoolsInfo.put("孙悟空", schools); 22 //遍历Map时,Map的key是String类型,value是List<String>类型 23 schoolsInfo.forEach((key, value) -> System.out.println(key + "-->" + value)); 24 } 25 }
深入泛型:
定义泛型接口、类:
泛型就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。
可以为任何类、接口增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的重要使用场所)。
1 //定义Apple类时使用了泛型声明 2 public class Apple<T>{ 3 //使用T类型形参定义实例变量 4 private T info; 5 public Apple(){} 6 //下面方法中使用T类型形参来定义构造器 7 public Apple(T info){ 8 this.info = info; 9 } 10 public void setInfo(T info){ 11 this.info = info; 12 } 13 public T getInfo(){ 14 return this.info; 15 } 16 public static void main(String[] args){ 17 //由于传给T形参的是String,所以构造器参数只能是String 18 Apple<String> a1 = new Apple<>("苹果"); 19 System.out.println(a1.getInfo()); 20 //由于传给T形参的是Double,所以构造器参数只能是Double或double 21 Apple<Double> a2 = new Apple<>(5.67); 22 System.out.println(a2.getInfo()); 23 } 24 }
当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。如:为Apple<T>类定义构造器,其构造器名依然是Apple,而不
是Apple<T>!调用该构造器是却可以使用Apple<T>的形式,应该为T形参传入实际的类型参数。
从泛型类派生子类:
创建带泛型声明的接口、父类后,可以为该接口创建实现类或派生子类。需要注意的是:当使用这些接口、父类是不能再包含类型形参<T>:
//定义类A继承Apple类,Apple类不能跟类型形参
public class A extends Apple<T>{ }//这是错误的
定义方法时,方法中的形参数据,被称为形参或数据形参。在使用方法时,必须为这些形参传入实际的数据。
类型形参也一样,在定义类、接口、方法时可以声明类型形参如:<T>,但使用这些类、接口、方法时应该为该类型形参传入实际的类型,如:<String>。
//使用Apple类时,为T形参传入String类型
public class A extends Apple<String>{ }
也可以不为类型形参传入实际的类型:
//使用Apple类时,没有为T形参传入实际的类型参数
public class A extends Apple{ }
若从Apple<String>类派生子类,则在Apple类中所有使用T类型形参的地方都将被替换成String类型,那么它的子类将会继承到String getInfo()和void setInfo(String info)两
个方法,若子类需要重写父类的方法,就必须注意这一点:
1 public class A1 extends Apple<String>{ 2 //正确重写了父类的方法,返回值 3 //与父类Apple<String>的返回值完全相同 4 public String getInfo(){ 5 return "子类" + super.getInfo(); 6 } 7 /* 8 //下面方法是错误的,重写父类方法时返回值类型不一致 9 public Object getInfo(){ 10 return "子类"; 11 } 12 */ 13 }
若使用Apple类没有传入实际的类型参数,java编译器可能发出警告:使用了未经检查或不安全的操作——这就是泛型检查的警告。若希望看到该警告的更详细信息,可
以为javac命令增加-Xlint:unchecked选项来实现。此时系统会把Apple<T>类里的T类型参数当成Object类型处理:
1 public class A2 extends Apple{ 2 //重写父类的方法 3 public String getInfo(){ 4 //super.getInfo()方法返回值是Object类型 5 //所以加toString()才返回String类型 6 return super.getInfo().toString(); 7 } 8 }
并不存在泛型类:
1 import java.util.List; 2 import java.util.ArrayList; 3 4 public class GeneicCompare{ 5 public static void main(String[] args){ 6 //分别创建List<String>对象和List<Integer>对象 7 List<Integer> list1 = new ArrayList<>(); 8 List<String> list2 = new ArrayList<>(); 9 //调用getClass()方法来比较list1和list2的类是否相等 10 System.out.println(list1.getClass() == list2.getClass()); 11 } 12 }
List<String> 和 List<Integer>的类是相同的,不管泛型的实际类型参数是什么,它们在运行时总有同样的类。
不管为泛型类型参数传入哪一种类型实参,对Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此不能在静态方法、静态初始化块或者静态变量
的声明和初始化中使用类型参数:
1 public class R<T>{ 2 //下面代码是错误的,不能在静态变量中使用类型形参 3 static T info; 4 T age; 5 public void foo(T msg){ } 6 //下面代码是错误的,不能在静态方法声明中使用类型形参 7 public static void bar(T msg){ } 8 }
由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类:
1 import java.util.Collection; 2 import java.util.ArrayList; 3 4 public class GeneicErrorInstanceof{ 5 public static void main(String[] args){ 6 Collection<String> cs = new ArrayList<>(); 7 if(cs instanceof ArrayList){ } 8 if(cs instanceof ArrayList<String>){ } 9 } 10 }
类型通配符:
当使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参。若没有传入类型实际参数,编译器就会提出泛型警告。假设需要定义一
个方法,该方法里有一个集合形参,集合形参是不确定的 ,怎样定义?
1 import java.util.List; 2 import java.util.ArrayList; 3 4 public class GeneicTest{ 5 public void test(List c){ 6 for(int i = 0; i < c.size(); i++){ 7 System.out.println(c.get(i)); 8 } 9 } 10 11 public static void main(String[] args){ 12 List<String> list = new ArrayList<>(); 13 list.add("疯狂Java讲义"); 14 list.add("疯狂Android讲义"); 15 GeneicTest gt = new GeneicTest(); 16 gt.test(list); 17 } 18 }
更改上面代码,让List带上类型参数<Object>:
1 import java.util.List; 2 import java.util.ArrayList; 3 4 public class GeneicTest{ 5 public void test(List<Object> c){ 6 for(int i = 0; i < c.size(); i++){ 7 System.out.println(c.get(i)); 8 } 9 } 10 11 public static void main(String[] args){ 12 List<String> list = new ArrayList<>(); 13 list.add("疯狂Java讲义"); 14 list.add("疯狂Android讲义"); 15 GeneicTest gt = new GeneicTest(); 16 gt.test(list); 17 } 18 }
上面的编译错误,表明List<String>对象不能被当成List<Object>对象使用。
若Foo是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G<Foo>并不是G<Bar>的子类型!这一点非常值得注意,因为它与大部分人的习惯认为是不
同的。
1 public class ArrayErr{ 2 public static void main(String[] args){ 3 //定义一个Integer数组 4 Integer[] ia = new Integer[5]; 5 //可以把一个Integer[]数组赋给Number[]变量 6 Number[] na = ia; 7 //下面代码编译正常,但运行时会引发ArrayStoreException异常 8 //因为0.5并不是Integer 9 na[0] = 1; 10 na[1] = 0.5; 11 } 12 }
数组允许将子类引用变量赋值给父类引用变量,但是父类引用变量存储的对象必须是子类对象,否则报错。
但是,Java在泛型设计时进行了改进,它不在允许吧List<Integer>引用变量赋值给List<Number>变量,这在根源上杜绝了类型不匹配的错误。
数组和泛型有所不同,假设Foo是Bar的一个子类型(子类或者子接口),那么Foo[] 依然是Bar[] 的子类型;但G<Foo>不是G<Bar>的子类型。
使用类型通配符:
为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?>(意思是元素类型未知的List)
这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。可以将上面的方法改为如下:
1 import java.util.List; 2 import java.util.ArrayList; 3 4 public class GeneicTest{ 5 public void test(List<?> c){ 6 for(int i = 0; i < c.size(); i++){ 7 System.out.println(c.get(i)); 8 } 9 } 10 11 public static void main(String[] args){ 12 List<String> list = new ArrayList<>(); 13 list.add("疯狂Java讲义"); 14 list.add("疯狂Android讲义"); 15 GeneicTest gt = new GeneicTest(); 16 gt.test(list); 17 } 18 }
但是这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素加入到其中。如下面代码会引起错误:
1 import java.util.List; 2 import java.util.ArrayList; 3 4 public class GeneicTest1{ 5 public static void main(String[] args){ 6 List<?> list = new ArrayList<String>(); 7 list.add("疯狂Java讲义"); 8 } 9 }
设定类型通配符的上限:
当直接使用List<?>这种形式,即表明这个List集合可以是任何泛型List的父类。但还有一种特殊的情形,程序不希望这个List<?>是任何泛型List父类,只希望它代表某一类
泛型List的父类:
1 //定义一个抽象类Shape 2 public abstract class Shape{ 3 public abstract void draw(Canvas c); 4 }
1 //定义Shape的子类Circle 2 public class Circle extends Shape{ 3 //实现画图方法,以打印字符串来模拟画图方法实现 4 public void draw(Canvas c){ 5 System.out.println("在画布" + c + "上画个圆"); 6 } 7 }
1 //定义Shape的子类Rectangle 2 public class Rectangle extends Shape{ 3 public void draw(Canvas c){ 4 //实现画图方法,以打印字符串来模拟画图方法实现 5 System.out.println("把一个矩形画在画布" + c + "上"); 6 } 7 }
上面的错误关键在于List<Circle>并不是List<Shape>的子类,所以不能把List<Circle>对象当成List<Shape>使用。为了表示List<Circle>的父类,可以考虑使用List<?>:
1 import java.util.List; 2 import java.util.ArrayList; 3 4 public class Canvas{ 5 //同时在画布上绘制多个形状 6 public void drawAll(List<?> shapes){ 7 for(Object obj : shapes){ 8 Shape s = (Shape) obj; 9 s.draw(this); 10 } 11 } 12 13 public static void main(String[] args){ 14 //下面的代码将会引起错误 15 //drawAll方法的形参类型是List<Shape>而不是List<Circle> 16 List<Circle> circleList = new ArrayList<>(); 17 Canvas c = new Canvas(); 18 //不能把List<Circle>当成List<Shape>使用 19 c.drawAll(circleList); 20 } 21 }
上面程序使用了通配符来表示所有的类型。问题是上面的方法实现体显得非常臃肿且繁琐:使用了泛型还需要强制类型转换。
实际上需要一种泛型表示方法,它可以表示所有Shape泛型List的父类,即List<?>可以匹配所有Shape和Shape的子类的List集合,而不用使用强制类型转换。
Java为泛型提供了被限制的泛型通配符。被限制的通配符表示如下:
//它表示所有Shape泛型List的父类
List<? extends Shape>
有了这种被限制的泛型通配符,把上面的Canvas程序改成如下:
1 import java.util.List; 2 import java.util.ArrayList; 3 4 public class Canvas{ 5 //同时在画布上绘制多个形状 6 public void drawAll(List<? extends Shape> shapes){ 7 for(Shape s : shapes){ 8 s.draw(this); 9 } 10 } 11 12 public static void main(String[] args){ 13 //下面的代码将会引起错误 14 //drawAll方法的形参类型是List<Shape>而不是List<Circle> 15 List<Circle> circleList = new ArrayList<>(); 16 Canvas c = new Canvas(); 17 //不能把List<Circle>当成List<Shape>使用 18 c.drawAll(circleList); 19 } 20 }
List<? extends Shape>:意思是List<>尖括号中只要是Shape或Shape的子类都可以。
但是不能对List<? extends Shape> shapes直接进行操作:
1 import java.util.List; 2 import java.util.ArrayList; 3 4 public class Canvas{ 5 //同时在画布上绘制多个形状 6 public void drawAll(List<? extends Shape> shapes){ 7 for(Shape s : shapes){ 8 s.draw(this); 9 } 10 } 11 12 public void addRectangle(List<? extends Shape> shapes){ 13 //下面代码引起编译错误 14 shapes.add(0, new Rectangle()); 15 } 16 17 public static void main(String[] args){ 18 //下面的代码将会引起错误 19 //drawAll方法的形参类型是List<Shape>而不是List<Circle> 20 List<Circle> circleList = new ArrayList<>(); 21 Canvas c = new Canvas(); 22 //不能把List<Circle>当成List<Shape>使用 23 c.drawAll(circleList); 24 } 25 }
设定类型形参的上限:
Java泛型不仅允许在使用通配符形参时设定上限,而且还可以在定义类型形参时设定上限,用于表示给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类
1 //定义Apple类时使用了泛型声明 2 public class Apple<T extends Number>{ 3 //使用T类型形参定义实例变量 4 private T info; 5 public static void main(String[] args){ 6 Apple<Integer> ai = new Apple<>(); 7 Apple<Double> ad = new Apple<>(); 8 //下面代码将引发编译异常,下面代码试图把String类型传给T形参 9 //但String不是Number的子类型,所以引起编译错误 10 Apple<String> as = new Apple<>(); 11 } 12 }
在一种极端情况下,程序需要为类型形参设定多个上限(至多有一个父类上限,可以有多个接口上限),表明该类型形参必须是其父类的子类(父类本身也可以),并且
实现了多个上限接口:
1 //表明T类型必须是Number类或其子类,并必须实现java.io.Serializable接口 2 public class Apple<T extends Number & java.io.Serializable>{ 3 ... 4 }
与类同时继承父类、实现接口类似,为类型形参指定多个上限时,所有接口上限必须位于类上限之后。即若需要为类型形参指定类上限,类上限必须位于第一位。
泛型方法:
Java5提供了对泛型方法的支持。
定义泛型方法格式:
1 //泛型方法,在声明方法时定义一个或多个类型形参,格式如下 2 修饰符 <T, S> 返回值类型 方法名(形参列表){ 3 //方法体... 4 }
泛型方法举例:
1 static <T> void fromArrayToCollection(T[] a, Collection<T> c){ 2 for(T o : a){ 3 c.add(o); 4 } 5 }
泛型方法完整程序用法:
1 import java.util.Collection; 2 import java.util.ArrayList; 3 4 public class GenericMethodTest{ 5 //声明一个泛型方法,该泛型方法中带一个T类型形参 6 static <T> void fromArrayToCollection(T[] a, Collection<T> c){ 7 for (T o : a){ 8 c.add(o); 9 } 10 } 11 12 public static void main(String[] args){ 13 Object[] oa = new Object[100]; 14 Collection<Object> co = new ArrayList<>(); 15 //下面代码中T代表Object类型 16 fromArrayToCollection(oa, co); 17 String[] sa = new String[100]; 18 Collection<String> cs = new ArrayList<>(); 19 //下面代码中T代表String类型 20 fromArrayToCollection(sa, cs); 21 //下面代码中T代表Object类型 22 fromArrayToCollection(sa, co); 23 Integer[] ia = new Integer[100]; 24 Float[] fa = new Float[100]; 25 Number[] na = new Number[100]; 26 Collection<Number> cn = new ArrayList<>(); 27 //下面代码中T代表Number类型 28 fromArrayToCollection(ia, cn); 29 //下面代码中T代表Number类型 30 fromArrayToCollection(fa, cn); 31 //下面代码中T代表Number类型 32 fromArrayToCollection(na, cn); 33 //下面代码中T代表Object类型 34 fromArrayToCollection(na, co); 35 //下面代码中T代表String类型,但na是一个Number数组 36 //因为Number既不是String类型 37 //也不是它的子类,所以出现编译错误 38 fromArrayToCollection(na, cs); 39 } 40 }
数组T[]可以是其子类数组,Collection<T>只能是同类T。
为了让编译器可以准确地判断出泛型方法中类型形参的类型,不要制造迷惑,一旦系统迷惑,就是你错了!如下:
1 import java.util.Collection; 2 import java.util.List; 3 import java.util.ArrayList; 4 5 public class ErrorTest{ 6 //声明一个泛型方法,该泛型方法中带一个T类型形参 7 static <T> void test(Collection<T> from, Collection<T> to){ 8 for(T ele : from){ 9 to.add(ele); 10 } 11 } 12 13 public static void main(String[] args){ 14 List<Object> ao = new ArrayList<>(); 15 List<String> as = new ArrayList<>(); 16 //下面代码将产生编译错误 17 test(as, ao); 18 } 19 }
上面程序调用test()方法传入两个实际参数,as是List<String>,ao是List<Object>,与test(Collection<T> a, Collection<T> c)相比,编译器无法正确识别T所代表的实际类型。
虽然String是Object的子类,但是集合中没有使用类型形参的上限,所以T只能代表String或Object类中的一个,不能代表两个。
改写为如下程序:
1 import java.util.Collection; 2 import java.util.List; 3 import java.util.ArrayList; 4 5 public class ErrorTest{ 6 //声明一个泛型方法,该泛型方法中带一个T类型形参 7 static <T> void test(Collection<? extends T> from, Collection<T> to){ 8 for(T ele : from){ 9 to.add(ele); 10 } 11 } 12 13 public static void main(String[] args){ 14 List<Object> ao = new ArrayList<>(); 15 List<String> as = new ArrayList<>(); 16 //下面代码将产生编译错误 17 test(as, ao); 18 } 19 }
程序中test(Collection<? extends T> from, Collection<T> to):只要前一个Collection集合里的元素类型是后一个Collection集合里元素类型的子类即可。
那么什么时候使用泛型方法?什么时候使用类型通配符?
泛型方法和类型通配符的区别:
大多数时候都可以使用泛型方法来代替类型通配符。如,Java的Collection接口中的两个方法:
1 public interface Collection<E>{ 2 boolean containsAll(Collection<?> c); 3 boolean addAll(Collection<? extends E> c); 4 }
上面集合中两个方法的形参都采用了类型通配符的形式,也可以采用泛型方法的形式:
1 public interface Collection<E>{ 2 <T> boolean containsAll(Collection<T> c); 3 <T extends E>boolean addAll(Collection<T> c); 4 }
若方法中一个形参(a)的类型或返回值类型依赖于另一个形参(b)的类型,则形参(b)的类型声明不应该是通配符,如:
public static <T> void copy(List<T> dest, List<? extends T> src) { ... }//这里dest形参的类型不能是通配符?,否则两个形参类型模糊。
上面的代码可以改为如下:
public static <T, S extends T> void copy(List<T> dest, List<S> src) { ... }//正是因为这里的S类型形参只使用了一次,所以没有存在的必要,直接用类型通配符代替即可
Java7的“菱形”语法与泛型构造器:
泛型构造器:
1 class Foo{ 2 public <T> Foo(T t){ 3 System.out.println(t); 4 } 5 } 6 7 public class GenericConstructor{ 8 public static void main(String[] args){ 9 //泛型构造器中的T参数为String 10 new Foo("疯狂Java讲义"); 11 //泛型构造器中的T参数为Integer 12 new Foo(200); 13 //显示指定泛型构造器中的T参数为String 14 //传给Foo构造器的实参也是String对象,完全正确 15 new <String> Foo("疯狂Android讲义"); 16 //显示指定泛型构造器的T参数为String 17 //但传给Foo构造器的实参是Double独享,下面代码是错误的 18 new <String> Foo(12.3); 19 } 20 }
Java7新增的“菱形”语法,允许调用构造器时在构造器后使用<>来代表泛型信息:
1 class MyClass<E>{ 2 public <T> MyClass(T t){ 3 System.out.println("t 参数的值为:" + t); 4 } 5 } 6 7 public class GenericDiamondTest{ 8 public static void main(String[] args){ 9 //MyClass类声明中的E形参是String类型 10 //泛型构造器中声明的T参数是Integer类型 11 MyClass<String> mc1 = new MyClass<>(5); 12 //显示指定泛型构造器中声明的T形参是Integer类型 13 MyClass<String> mc2 = new <Integer> MyClass<String>(5); 14 //MyClass类声明中的E形参是String类型 15 //若显示指定泛型构造器中声明的T形参是Integer类型 16 //此时就不能使用“菱形”语法,下面代码是错误的 17 MyClass<String> mc3 = new <Integer> MyClass<>(5); 18 } 19 }
i. 和使用普通泛型方法一样没区别,一种是显式指定泛型参数,另一种是隐式推断,如果是显式指定则以显式指定的类型参数为准,如果传入的参数的类型和指定的类型实参不符,将会编译报错;
!典型示例:A a = new <String>A("lala"); // 菱形实参还是写在方法名之前
ii. 这里唯一需要特殊注明的就是,如果构造器是泛型构造器,同时该类也是一个泛型类的情况下应该如何使用泛型构造器:
a. 因为泛型构造器可以显式指定自己的类型参数(需要用到菱形,放在构造器之前),而泛型类自己的类型实参也需要指定(菱形放在构造器之后),这就同时出现了两个菱形了,这就会有一些小问题,具体用法再这里总结一下;
b. 这里使用的类是这样定义的:public A<T> { public <E> A(E e) { ... } }
b. 全指定,例如:A<String> a = new <Integer>A<String>(15); // 所有类型实参全部显式指定,Integer代表E,String代表T
c. 全隐藏,例如:A<String> a = new A<>(15); // 可以隐式推断出Integer是E,String是T
d. 半隐藏,例如:A<String> a = new A<String>(15); // 还是可以推断出E是Integer,而String是T
e. 上面的叫做半隐藏T,但是不能半隐藏E,例如:A<String> a = new <Integer>A<>(15); // 虽然也可以正确推断,但是这种语法不允许!!会直接编译报错!
!!因此这里麻烦的是需要稍微记忆一下,不能半隐藏E
!!平时使用的时候就使用其中一种,推荐是全隐藏,越简洁越好,就是用一种就不会增加记忆负担;
上面一段泛型构造器篇幅,参考网址:http://blog.csdn.net/lirx_tech/article/details/51603531
设定通配符下限:
现在要实现一个工具方法:将src集合里的元素复制到dest集合里的功能,分析:因为dest集合可以保存src集合里的所有元素,所以dest集合元素的类型是src集合元素类
型的父类,代码如下:
1 public static <T> void copy(Collection<T> dest, Collection<? extends T> src){ 2 for(T ele : src){ 3 dest.add(ele); 4 } 5 }
现在又要让该方法具有返回值,返回最后一个被复制的元素,代码如下:
1 public static <T> T copy(Collection<T> dest, Collection<? extends T> src){ 2 T last = null; 3 4 for(T ele : src){ 5 last = ele; 6 dest.add(ele); 7 } 8 9 return last; 10 }
表面上看起来方法实现了返回值的功能,但是若用下面的程序验证,则会报错:
1 List<Number> ln = new ArrayList<>(); 2 List<Integer> li = new ArrayList<>(); 3 //下面代码引起编译错误 4 Integer last = copy(ln, li);
ln类型是List<Number>,li类型是List<Integer>,与copy()方法比较后,确认T类型形参是Number,返回值类型也是Number类型,所以Integer last不能赋值为Number类型
解决方法:可以这样理解两个集合参数之间的依赖关系:不管src集合元素的类型是什么,只要dest集合元素的类型与前者相同或是前者的父类即可。为了表达这种约束关系
Java允许设定通配符的下限:<? super Type>,这个通配符表示它必须是Type本身,或是Type的父类,下面程序采用设定通配符下限方式改写了前面程序:
1 import java.util.List; 2 import java.util.ArrayList; 3 import java.util.Collection; 4 5 public class MyUtils{ 6 //下面dest集合元素的类型必须与src集合元素的类型相同,或是其父类 7 public static <T> T copy(Collection<? super T> dest, Collection<T> src){ 8 T last = null; 9 for(T ele : src){ 10 last = ele; 11 dest.add(ele); 12 } 13 return last; 14 } 15 16 public static void main(String[] args){ 17 List<Number> ln = new ArrayList<>(); 18 List<Integer> li = new ArrayList<>(); 19 li.add(5); 20 //此处可准确地知道最后一个被复制的元素时Integer类型 21 //与src集合元素的类型相同 22 Integer last = copy(ln, li); 23 System.out.println(ln); 24 } 25 }
这样可以保证程序可以推断出最后一个被复制的元素类型是Integer,而不是Number类型。
泛型方法与方法重载:
因为泛型既允许设定通配符的上限,也允许设定通配符的下限,所以允许在一个类里包含如下两个方法定义:
public class MyUtils{ public static <T> void copy(Collection<T> dest, Collection<? extends T> src){ } public static <T> T copy(Collection<? super T> dest, Collection<T> src) { } }
上面MyUtils类中包含两个copy()方法,但调用这个方法就会引起编译错误:
List<Number> ln = new ArrayList<>(); List<Integer> li = new ArrayList<>(); copy(ln, li);
编译器无法确定这行代码想要调用哪个copy()方法。
泛型与数组:
参考网址:http://blog.csdn.net/orzlzro/article/details/7017435