public class ListErr { public static void main(String[] args) { // 创建一个只想保存字符串的List集合 List strList = new ArrayList(); strList.add("布达佩斯"); strList.add("布拉格"); // "不小心"把一个Integer对象"丢进"了集合 strList.add(34); // ① strList.forEach(str -> System.out.println(((String)str).length())); // ② } }
上述程序创建了一个List集合,且该List集合保存字符串对象——但程序不能进行任何限制,如果程序在①处“不小心”把一个Integer对象"丢进"了List集合,这将导致程序在②处引发ClassCastException异常,因为程序试图把一个Integer对象转换为String类型
参数化类型,允许程序在创建集合时指定集合元素的类型。Java的参数化类型被称为泛型(Generic)
class GenericList { public static void main(String[] args) { // 创建一个只想保存字符串的List集合 List<String> strList = new ArrayList<>(); // ① strList.add("布达佩斯"); strList.add("布拉格"); // 下面代码将引起编译错误 strList.add(34); // ② strList.forEach(str -> System.out.println(((String)str).length())); // ③ } }
strList集合只能保存字符串对象,不能保存其他类型的对象。创建特殊集合的方法是:在集合接口、类后增加尖括号,尖括号里放一个数据类型,即表明这个集合接口、集合类只能保存特定类型的对象。
①类型声明,在创建这个ArrayList对象时也指定了一个类型参数;②引发编译异常;③不需要进行强制类型转换
泛型使程序更加简洁,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换
Java允许在构造器后不需要带完整的泛型信息,只要给出一对尖括号(<>)即可,Java可以推断尖括号里应该是什么泛型信息
public class DiamondTest { public static void main(String[] args) { // Java自动推断出ArrayList的<>里应该是String List<String> countries = new ArrayList<>(); countries.add("法兰西第五共和国"); countries.add("西班牙王国"); // 遍历countries集合,集合元素就是String类型 countries.forEach(ele -> System.out.println(ele.length())); // Java自动推断出HashMap的<>里应该是String , List<String> Map<String , List<String>> citiesInfo = new HashMap<>(); // Java自动推断出ArrayList的<>里应该是String List<String> cities = new ArrayList<>(); cities.add("巴黎"); cities.add("巴塞罗那"); citiesInfo.put("Bienvenue" , cities); // 遍历Map时,Map的key是String类型,value是List<String>类型 citiesInfo.forEach((key , value) -> System.out.println(key + "-->" + value)); } }
所谓泛型:就是允许在定义类、接口、方法时指定类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)
List接口、Iterator接口、Map的代码片段
// 定义接口时指定了一个类型形参,该形参名为E public interface List<E> { // 在该接口里,E可作为类型使用 // 下面方法可以使用E作为参数类型 void add(E x); Iterator<E> iterator(); // ① } // 定义接口时指定了一个类型形参,该形参为E public interface Iterator<E> { // 在该接口里E完全可以作为类型使用 E next(); boolean hasNext(); } // 定义该接口时指定了两个类型形参,其形参名为K、V public interface Map<K, V> { // 在该接口里K、V完全可以作为类型使用 Set<K> keySet(); // ② V put(K key,V value); }
上面代码说明泛型实质:允许在定义接口、类时声明类型形参,类型形参在整个接口、类体内可当成类型使用
包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数个逻辑上的子类,但这种子类在物理上并不存在
可以为任何类、接口增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的重要使用场所)
// 定义Onmyoji类时使用了泛型声明 public class Onmyoji<T> { // 使用T类型形参定义实例变量 private T info; public Onmyoji(){} // 下面方法使用T类型形参来定义构造器 public Onmyoji(T info) { this.info = info; } public T getInfo() { return info; } public void setInfo(T info) { this.info = info; } public static void main(String[] args) { //由于传给T形参的是String,所以构造器参数只能是String Onmyoji<String> a1 = new Onmyoji<>("安倍晴明"); System.out.println(a1.getInfo()); // 由于传给T形参的是Double,所以构造器参数只能是Double或double Onmyoji<Double> a2 = new Onmyoji<>(520.1314); System.out.println(a2.getInfo()); } }
当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明
当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从父类派生子类。当使用这些接口、父类时不能再包含类型形参
// 定义类Shikigami继承Onmyoji类,Onmyoji类不能跟类型形参 public class Shikigami extends Onmyoji<T>{ } // 错误
方法中的形参(或数据形参)代表变量、常量、表达式等数据。定义方法时,可以声明数据形参;调用方法(使用方法)时,必须为这些数据形参传入实际的数据;与此类似的是,定义类、接口、方法时可以声明类型形参,使用类、接口、方法时应为类型形参传入实际的类型
// 使用Onmyoji类时为T形参传入String类型 public class Shikigami extends Onmyoji<String>{ } // 正确
调用方法时必须为所有的数据参数传入参数值,而使用类、接口时也可以不为类型形参传入实际的类型参数
// 使用Onmyoji类时,没有为T形参传入实际的类型参数 public class Shikigami extends Onmyoji{ } // 正确
子类需要重写父类的Getters和Setters方法
private String info; public String getInfo() { return "子类"+ super.getInfo(); } public void setInfo(String info) { this.info = info; }
ArrayList<String>类,是一种特殊的ArrayList类。该ArrayList<String>对象只能添加String对象作为集合元素。但实际上,系统并没有为ArrayList<String>生成新的class文件,而且也不会把ArrayList<String>当成新类来处理。因为不管泛型的时间类型参数是什么,它们在运行时总有同样的类(class)
// 分别创建List<String>对象和List<Integer>对象 List<String> l1 = new ArrayList<>(); List<Integer> l2 = new ArrayList<>(); // 调用getClass()方法来比较l1和l2的类是否相等 System.out.println(l1.getClass() == l2.getClass()); // 输出true
不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参
由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。
if(cs instanceof List<String>) { ... }
如果Foo是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G<Foo>并不是G<Bar>的子类型
数组和泛型有所不同,假设Foo是Bar的一个子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型;但G<Foo>不是G<Bar>的子类型
Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常
为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?>(意思是未知类型元素的List)。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型
public void test(List<?> c) { for(int i = 0; i < c.size(); i++) { System.out.println(c.get(i)); } }
现在使用任何类型的List来调用它,程序依然可以访问集合c中的元素,其类型是Object,这永远是安全的,因为不管List的真实类型是什么,它包含的都是 Object
这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素加入到其中
List<?> c = new ArrayList<String>(); // 下面程序引起编译错误 c.add(new Object());
因为程序无法确认c集合里元素的类型,所以不能向其中添加对象。根据前面的List<E>接口定义的代码可以发现:add ()方法由类型参数E作为集合的元素类型,所以传给add的参数必须是E类的对象或者其子类的对象。但因为在该例中不知道E是什么类型,所以程序无法将任何对象“丢进”该集合。唯一的例外是 null,它是所有引用类型的实例
另一方面,程序可以调用get()方法来返回List<?>集合指定索引处的元素,其返回值是一个未知类型,但可以肯定的是,它总是一个Object。因此,把get()的返回值赋值给一个Object类型的变量,或者放在任何希望是Object类型的地方都可以
List<Circle>并不是List<Shape>的子类型,所以不能把List<Circle>对象当成List<Shape>使用。为了表示List<Circle>的父类,使用List<? extends Shape>
List<? extends Shape>是受限通配符的例子,此处的问号(?)代表一个未知的类型,此处的未知类型一定是Shape的子类也可以是Shape,因此可以把shape称为这个通配符的上限(upper bound)
Java泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要门是该上限类型的子类
在一种更极端的情况下,程序需要为类型形参设定多个上限(至少有一个父类上限,可以有多个接口上限),表明该类型形参必须是其父类的子类(其父类本事也行),并且实现多个上限接口
//表明T类型必须是Number类或其子类,并必须实现java.io.Serializablepublic class Apple<T extends Number & java.io.Serilizable> { ... }
与类同时继承父类、实现接口类似的是:为类型形参指定多个上限,所有的接口上限必须位于类上限之后。也就是说,如果需要为类型形参指定类上限,类上限必须位于第一位
Java不允许把对象放进一个未知类型的集合中
所谓泛型方法,就是在声明方法时定义一个或多个类型形参
修饰符 <T, S> 返回值类型 方法名(形参列表) { // 方法体... }