摘要:唯一的例外是,它是所有引用类型的实例另一方面,程序可以调用方法来返回集合指定索引处的元素,其返回值是一个未知类型,但可以肯定的是,它总是一个。
泛型入门 编译时不检查类型的异常
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集合 ListstrList = new ArrayList<>(); // ① strList.add("布达佩斯"); strList.add("布拉格"); // 下面代码将引起编译错误 strList.add(34); // ② strList.forEach(str -> System.out.println(((String)str).length())); // ③ } }
strList集合只能保存字符串对象,不能保存其他类型的对象。创建特殊集合的方法是:在集合接口、类后增加尖括号,尖括号里放一个数据类型,即表明这个集合接口、集合类只能保存特定类型的对象
①类型声明,在创建这个ArrayList对象时也指定了一个类型参数;②引发编译异常;③不需要进行强制类型转换
泛型使程序更加简洁,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换
Java7泛型的“菱形”语法Java允许在构造器后不需要带完整的泛型信息,只要给出一对尖括号(<>)即可,Java可以推断尖括号里应该是什么泛型信息
public class DiamondTest { public static void main(String[] args) { // Java自动推断出ArrayList的<>里应该是String List深入泛型countries = new ArrayList<>(); countries.add("法兰西第五共和国"); countries.add("西班牙王国"); // 遍历countries集合,集合元素就是String类型 countries.forEach(ele -> System.out.println(ele.length())); // Java自动推断出HashMap的<>里应该是String , List Map > citiesInfo = new HashMap<>(); // Java自动推断出ArrayList的<>里应该是String List cities = new ArrayList<>(); cities.add("巴黎"); cities.add("巴塞罗那"); citiesInfo.put("Bienvenue" , cities); // 遍历Map时,Map的key是String类型,value是List 类型 citiesInfo.forEach((key , value) -> System.out.println(key + "-->" + value)); } }
所谓泛型:就是允许在定义类、接口、方法时指定类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)
定义泛型接口、类List接口、Iterator接口、Map的代码片段
// 定义接口时指定了一个类型形参,该形参名为E public interface List{ // 在该接口里,E可作为类型使用 // 下面方法可以使用E作为参数类型 void add(E x); Iterator iterator(); // ① } // 定义接口时指定了一个类型形参,该形参为E public interface Iterator { // 在该接口里E完全可以作为类型使用 E next(); boolean hasNext(); } // 定义该接口时指定了两个类型形参,其形参名为K、V public interface Map { // 在该接口里K、V完全可以作为类型使用 Set keySet(); // ② V put(K key,V value); }
上面代码说明泛型实质:允许在定义接口、类时声明类型形参,类型形参在整个接口、类体内可当成类型使用
包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数个逻辑上的子类,但这种子类在物理上并不存在
可以为任何类、接口增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的重要使用场所)
// 定义Onmyoji类时使用了泛型声明 public class Onmyoji{ // 使用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 a1 = new Onmyoji<>("安倍晴明"); System.out.println(a1.getInfo()); // 由于传给T形参的是Double,所以构造器参数只能是Double或double Onmyoji a2 = new Onmyoji<>(520.1314); System.out.println(a2.getInfo()); } }
当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明
从泛型类派生子类当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从父类派生子类。当使用这些接口、父类时不能再包含类型形参
// 定义类Shikigami继承Onmyoji类,Onmyoji类不能跟类型形参 public class Shikigami extends Onmyoji{ } // 错误
方法中的形参(或数据形参)代表变量、常量、表达式等数据。定义方法时,可以声明数据形参;调用方法(使用方法)时,必须为这些数据形参传入实际的数据;与此类似的是,定义类、接口、方法时可以声明类型形参,使用类、接口、方法时应为类型形参传入实际的类型
// 使用Onmyoji类时为T形参传入String类型 public class Shikigami extends Onmyoji{ } // 正确
调用方法时必须为所有的数据参数传入参数值,而使用类、接口时也可以不为类型形参传入实际的类型参数
// 使用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
// 分别创建List对象和List 对象 List l1 = new ArrayList<>(); List l2 = new ArrayList<>(); // 调用getClass()方法来比较l1和l2的类是否相等 System.out.println(l1.getClass() == l2.getClass()); // 输出true
不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参
public class R{ static T info; // 代码错误,不能在静态变量声明中使用类型形参 T age; public void foo(T msg){} public static void bar(T msg){} // 代码错误,不能在静态方法声明中使用类型形参 }
由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。
if(cs instanceof List类型通配符) { ... }
如果Foo是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G
数组和泛型有所不同,假设Foo是Bar的一个子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型;但G
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(); // 下面程序引起编译错误 c.add(new Object());
因为程序无法确认c集合里元素的类型,所以不能向其中添加对象。根据前面的List
另一方面,程序可以调用get()方法来返回List>集合指定索引处的元素,其返回值是一个未知类型,但可以肯定的是,它总是一个Object。因此,把get()的返回值赋值给一个Object类型的变量,或者放在任何希望是Object类型的地方都可以
设定类型通配符的上限List
List extends Shape>是受限通配符的例子,此处的问号(?)代表一个未知的类型,此处的未知类型一定是Shape的子类也可以是Shape,因此可以把shape称为这个通配符的上限(upper bound)
设定类型形参的上限Java泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要门是该上限类型的子类
在一种更极端的情况下,程序需要为类型形参设定多个上限(至少有一个父类上限,可以有多个接口上限),表明该类型形参必须是其父类的子类(其父类本事也行),并且实现多个上限接口
//表明T类型必须是Number类或其子类,并必须实现java.io.Serializablepublic class Apple{ ... }
与类同时继承父类、实现接口类似的是:为类型形参指定多个上限,所有的接口上限必须位于类上限之后。也就是说,如果需要为类型形参指定类上限,类上限必须位于第一位
泛型方法 定义泛型方法Java不允许把对象放进一个未知类型的集合中
所谓泛型方法,就是在声明方法时定义一个或多个类型形参
修饰符返回值类型 方法名(形参列表) { // 方法体... }
把上面方法的格式和普通方法的格式进行对比,不难发现泛型方法的方法签名比普通方法的方法签名多了类型形参声明,类型形参声明以尖括号括起来,多个类型形参之间以逗号隔开,所有的类型形参声明放在方法修饰符和方法返回值类型之间
public class GenericMethodTest { // 声明一个泛型方法,该泛型方法中带一个T类型形参, staticvoid fromArrayToCollection(T[] a, Collection c) { for (T o : a) { c.add(o); } } public static void main(String[] args) { Object[] oa = new Object[100]; Collection
为了让编译器能准确地推断出泛型方法中类型形参的类型,不要制造迷惑!系统一旦迷惑了,就是你错了!看如下程序
public class ErrorTest { // 声明一个泛型方法,该泛型方法中带一个T类型形参 staticvoid test(Collection from, Collection to) { for (T ele : from) { to.add(ele); } } public static void main(String[] args) { List
上面程序中定义了test方法,该方法用于将前一个集合里的元素复制到下一个集合中,该方法中的两个形参 from、to 的类型都是 Collection
上面程序中调用test方法传入了两个实际参数,其中as的数据类型是List
public class RightTest { // 声明一个泛型方法,该泛型方法中带一个T形参 staticvoid test(Collection extends T> from , Collection to) { for (T ele : from) { to.add(ele); } } public static void main(String[] args) { List
上面代码改变了test方法签名,将该方法的前一个形参类型改为 Collection extends T>,这种采用类型通配符的表示方式,只要test方法的前一个Collection集合里的元素类型是后一个Collection集合里元素类型的子类即可
泛型方法和类型通配符的区别大多数时候都可以使用泛型方法来代替类型通配符。例如,对于 Java 的 Collection 接口中两个方法定义:
public interface Collection{ boolean containAll(Collection> c); boolean addAll(Collection extends E> c); ... }
上面集合中两个方法的形参都采用了类型通配符的形式,也可以采用泛型方法的形式,如下所示
public interface Collection{ boolean containAll(Collection c); boolean addAll(Collection c); ... }
上面方法使用了
泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法
如果某个方法中一个形参(a)的类型或返回值的类型依赖于另一个形参(b)的类型,则形参(b)的类型声明不应该使用通配符----因为形参(a)或返回值的类型依赖于该形参(b)的类型,如果形参(b)的类型无法确定,程序就无法定义形参(a)的类型。在这种情况下,只能考虑使用在方法签名中声明类型形参 ——也就是泛型方法
也可以同时使用泛型方法和通配符,如Java的Collections.copy方法
public class Collections { public staticvoid copy(List dest, List extends T> src) { ... } ... }
上面copy方法中的dest和src存在明显的依赖关系,从源List中复制出来的元素,必须可以“丢进”目标List中,所以源List集合元素的类型只能是目标集合元素的类型的子类型或者它本身。但JDK定义src形参类型时使用的是类型通配符,而不是泛型方法。这是因为:该方法无须向src集合中添加元素,也无须修改src集合里的元素,所以可以使用类型通配符,不使用泛型方法
类型通配符与泛型方法(在方法签名中显式声明类型形参)还有一个显著的区别:类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的类型形参必须在对应方法中显式声明
Java7的“菱形”语法与泛型构造器Java允许在构造器签名中声明类型形参,这样就产生了所渭的泛型构造器。一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让Java根据数据参数的类型来“推断”类型形参的类型,而且程序员也可以显式地为构造器中的类型形参指定实际的类型
class Foo { publicFoo(T t) { System.out.println(t); } } public class GenericConstructor { public static void main(String[] args) { // 泛型构造器中的T参数为String。 new Foo("疯狂Java讲义"); // 泛型构造器中的T参数为Integer。 new Foo(200); // 显式指定泛型构造器中的T参数为String, // 传给Foo构造器的实参也是String对象,完全正确。 new Foo("疯狂Android讲义"); // 显式指定泛型构造器中的T参数为String, // 但传给Foo构造器的实参是Double对象,下面代码出错 new Foo(12.3); } }
前面介绍过 Java 7 新增的“菱形”语法,它允许调用构造器时在构造器后使用一对尖括号来代表泛型信息。但如果程序显式指定了泛型构造器中声明的类型形参的实际类型,则不可以使用“菱形”语法
class MyClass{ public MyClass(T t) { System.out.println("t参数的值为:" + t); } } public class GenericDiamondTest { public static void main(String[] args) { // MyClass类声明中的E形参是String类型。 // 泛型构造器中声明的T形参是Integer类型 MyClass mc1 = new MyClass<>(5); // 显式指定泛型构造器中声明的T形参是Integer类型, MyClass mc2 = new MyClass (5); // MyClass类声明中的E形参是String类型。 // 如果显式指定泛型构造器中声明的T形参是Integer类型 // 此时就不能使用"菱形"语法,下面代码是错的。 // MyClass mc3 = new MyClass<>(5); } }
上面程序中最后一行代码既指定了泛型构造器中的类型形参是 Integer 类型,又想使用“菱形”语法,所以这行代码无法通过编译
设定通配符下限实现将src集合里的元素复制到dest集合里的功能,因为dest集合可以保存src集合里的所有元素,所以dest集合元素的类型应该是src集合元素类型的父类,假设该方法需要一个返回值,返回最后一个被复制的元素。为了表示两个参数之间的类型依赖,考虑同时使用通配符、泛型参数来实现该方法
public staticT copy(Collection dest, Collection extends T> src) { T last = null; for (T ele : src) { last = ele; dest.add(ele); } return last; }
实际上有一个问题:当遍历src集合的元素时,src元素的类型是不确定的(但可以肯定它是T的子类),程序只能用T来笼统地表示各种src集合的元素类型
Listln = new ArrayList<>; List li = new ArrayList<>; // 下面代码引起编译错误 Integer last = copy(ln, li);
ln的类型是List
对于上面的copy方法,可以这样理解两个集合参数之间的依赖关系:不管src集合元素的类型是什么,只要dest集合元素的类型与前者相同或是前者的父类即可。Java允许设定通配符的下限: super Type>,这个通配符表示它必须是Type本身,或是Type的父类
public class MyUtils { // 下面dest集合元素类型必须与src集合元素类型相同,或是其父类 public staticT copy(Collection super T> dest , Collection src) { T last = null; for (T ele : src) { last = ele; dest.add(ele); } return last; } public static void main(String[] args) { List ln = new ArrayList<>(); List li = new ArrayList<>(); li.add(5); // 此处可准确的知道最后一个被复制的元素是Integer类型 // 与src集合元素的类型相同 Integer last = copy(ln , li); // ① System.out.println(ln); } }
使用这种语句,就可以保证程序的①处调用后推断出晟后一个被复制的元素类型是 Integer,而不是笼统的 Number 类型
泛型方法与方法重载因为泛型既允许设定通配符的上限,也允许设定通配符的下限,从而允许在一个类里包含如下两个方法定义
public class MyUtils { public staticvoid copy(Collection dest , Collection extends T> src) {...} // ① public static copy(Collection super T> dest, Collection src) {...} // ② }
MyUtils类中包含两个copy方法,这两个方法的参数列表存在一定的区别,但这种区别不是很明确:这两个方法的两个参数都是Collection对象,前一个集合里的集合元素类型是后一个集合里集合元素类型的父类。如果这个类仅包含这两个方法不会有任何错误,但只要调用这个方法就会引起编译错误
Listln = new ArrayList<>; List li = new ArrayList<>; copy(ln , li);
上面程序调用copy方法,但这个copy()方法既可以匹配①号copy方法,此时T类型参数的类型是 Number;也可以匹配②号copy()方法,此时T参数的类型是Integer。编译器无法确定这行代码想调用哪个copy()方法,所以这行代码将引起编译错误
Java8改进的类型推断Java8改进了泛型方法的类型推断能力,类型推断主要有如下两方面
可通过调用方法的上下文来推断类型参数的目标类型
可在方法调用链中,将推断得到的类型参数传递到最后一个方法
class MyUtil擦除和转换{ public static MyUtil nil() { return null; } public static MyUtil cons(Z head, MyUtil tail) { return null; } E head() { return null; } } public class InferenceTest { public static void main(String[] args) { // 可以通过方法赋值的目标参数来推断类型参数为String MyUtil ls = MyUtil.nil(); // 无需使用下面语句在调用nil()方法时指定类型参数的类型 MyUtil mu = MyUtil. nil(); // 可调用cons方法所需的参数类型来推断类型参数为Integer MyUtil.cons(42, MyUtil.nil()); // 无需使用下面语句在调用nil()方法时指定类型参数的类型 MyUtil.cons(42, MyUtil. nil()); // 希望系统能推断出调用nil()方法类型参数为String类型, // 但实际上Java 8依然推断不出来,所以下面代码报错 // String s = MyUtil.nil().head(); String s = MyUtil. nil().head(); } }
在严格的泛型代码里,带泛型声明的类总应该带着泛型参数。但是为了和古老的java代码保持一致,也就是说为了向下兼容,也允许在使用带泛型声明的类时不指定实际的类型参数。如果没有为这个泛型指定实际的类型参数,则该类型参数被称作raw type(原始类型),默认是声明该类型参数时指定的第一个上限类型
当把一个具体泛型信息的对象赋值给另外一个没有泛型信息的变量时,所有尖括号之间的类型信息都将被扔掉。比如说将一个List
class Apple{ T size; public Apple() { } public Apple(T size) { this.size = size; } public void setSize(T size) { this.size = size; } public T getSize() { return this.size; } } public class ErasureTest { public static void main(String[] args) { Apple a = new Apple<>(6); // ① // a的getSize()方法返回Integer对象 Integer as = a.getSize(); // 把a对象赋给Apple变量,丢失尖括号里的类型信息 Apple b = a; // ② // b只知道size的类型是Number Number size1 = b.getSize(); // 下面代码引起编译错误 Integer size2 = b.getSize(); // ③ } }
上面程序定义了一个带泛型声明的Apple类,其类型形参的上限是Number,这个类型形参用来定义Apple类的size变量。程序在①处创建了一个Apple对象,该Apple对象传入了Integer作为类型形参的值,所以调用a的getSize()方法时返回Integer类型的值。当把a赋给一个不带泛型信息的b变量时,编译器就会丢失a对象的泛型信息,即所有尖括号里的信息都会丢失——因为Apple的类型形参的上限是Number类,所以编译器依然知道b的getSize()方法返回Number类型,但具体是Number的哪个子类就不清楚了
从逻辑上来看,List
public class ErasureTest2 { public static void main(String[] args) { Listli = new ArrayList<>(); li.add(34); li.add(59); List list = li; // 下面代码引起“未经检查的转换”的警告,编译、运行时完全正常 List ls = list; // ① // 但只要访问ls里的元素,如下面代码将引起运行时异常 // ClassCastException System.out.println(ls.get(0)); } }
上面程序中定义了一个List
下面代码与上面代码的行为完全相似
public class ErasureTest2 { public static void main(String[] args) { Listli = new ArrayList<>(); li.add(34); li.add(59); System.out.println((String)li.get(0)); } }
程序从li中获取一个元素,并试图通过强制类型转换把它转换成一个String,将引发运行时异常。前面使用泛型代码时,系统与之存在完全相似的行为,所以引发相同的ClassCastException异常
泛型与数组Java泛型与一个很重要的设计原则——如果一段代码在编译时没有提出“[unchecked] 未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。正是基于这个原因,所以数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符。但可以声明元素类型包含类型变量或类型形参的数组。也就是说,只能声明List
Java不支持创建泛型数组
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/66419.html
摘要:引用泛型除了方法因不能使用外部实例参数外,其他继承实现成员变量,成员方法,方法返回值等都可使用。因此,生成的字节码仅包含普通的类,接口和方法。 为什么要使用泛型程序设计? 一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义类的对应类型;如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。----摘自原书Ordinary classes and meth...
摘要:知识点总结泛型知识点总结泛型泛型泛型就是参数化类型适用于多种数据类型执行相同的代码泛型中的类型在使用时指定泛型归根到底就是模版优点使用泛型时,在实际使用之前类型就已经确定了,不需要强制类型转换。 Java知识点总结(Java泛型) @(Java知识点总结)[Java, Java泛型] [toc] 泛型 泛型就是参数化类型 适用于多种数据类型执行相同的代码 泛型中的类型在使用时指定 泛...
摘要:静态变量是被泛型类的所有实例所共享的。所以引用能完成泛型类型的检查。对于这个类型系统,有如下的一些规则相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。事实上,泛型类扩展都不合法。 前言 和C++以模板来实现静多态不同,Java基于运行时支持选择了泛型,两者的实现原理大相庭径。C++可以支持基本类型作为模板参数,Java却只能接受类作为泛型参数;Java可以在泛型类的方法中取得...
摘要:泛型类在类的申明时指定参数,即构成了泛型类。换句话说,泛型类可以看成普通类的工厂。的作用就是指明泛型的具体类型,而类型的变量,可以用来创建泛型类的对象。只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。 什么是泛型? 泛型是JDK 1.5的一项新特性,它的本质是参数化类型(Parameterized Type)的应用,也就是说所操作的数据类型被指定为一个参数,...
摘要:可以看到,如果我们给泛型类制定了上限,泛型擦除之后就会被替换成类型的上限。相应的,泛型类中定义的方法的类型也是如此。参考语言类型擦除下界通配符和的区别 本篇博客主要介绍了Java类型擦除的定义,详细的介绍了类型擦除在Java中所出现的场景。 1. 什么是类型擦除 为了让你们快速的对类型擦除有一个印象,首先举一个很简单也很经典的例子。 // 指定泛型为String List list1 ...
阅读 3373·2021-11-22 09:34
阅读 2859·2021-10-09 09:43
阅读 1450·2021-09-24 09:47
阅读 2201·2019-08-30 12:53
阅读 1000·2019-08-29 14:00
阅读 3359·2019-08-29 13:17
阅读 2270·2019-08-28 18:00
阅读 1286·2019-08-26 12:00