Java泛型示例教程–泛型方法,类,接口
Java Genrics是Java 5中引入的最重要的功能之一。
如果您一直在使用Java Collections并使用版本5或者更高版本,请确保您已使用它。
Java中带有集合类的泛型非常简单,但是它提供了比仅创建集合类型更多的功能。
我们将在本文中尝试学习泛型的功能。
如果我们使用专业术语,对泛型的理解有时会变得混乱,因此,我将尽量保持其简单易懂。
1. Java中的泛型
Java 5中添加了泛型,以提供编译时类型检查并消除使用集合类时常见的ClassCastException
风险。
整个收集框架都进行了重写,以使用泛型进行类型安全。
让我们看看泛型如何帮助我们安全地使用集合类。
List list = new ArrayList(); list.add("abc"); list.add(new Integer(5)); //OK for(Object obj : list){ //type casting leading to ClassCastException at runtime String str=(String) obj; }
上面的代码可以很好地编译,但是在运行时会抛出ClassCastException,因为我们试图将列表中的Object强制转换为String,而其中一个元素是Integer类型。
在Java 5之后,我们使用如下收集类。
List<String> list1 = new ArrayList<String>(); //java 7 ? List<String> list1 = new ArrayList<>(); list1.add("abc"); //list1.add(new Integer(5)); //compiler error for(String str : list1){ //no type casting needed, avoids ClassCastException }
请注意,在创建列表时,我们已指定列表中元素的类型为String。
因此,如果我们尝试在列表中添加任何其他类型的对象,则该程序将引发编译时错误。
还要注意,在for循环中,我们不需要列表中元素的类型转换,因此在运行时删除了ClassCastException。
2. Java通用类
我们可以使用泛型类型定义自己的类。
泛型类型是通过类型进行参数化的类或者接口。
我们使用尖括号(<>)来指定type参数。
为了了解其好处,假设我们有一个简单的类:
package com.theitroad.generics; public class GenericsTypeOld { private Object t; public Object get() { return t; } public void set(Object t) { this.t = t; } public static void main(String args[]){ GenericsTypeOld type = new GenericsTypeOld(); type.set("hyman"); String str = (String) type.get(); //type casting, error prone and can cause ClassCastException } }
请注意,在使用此类时,我们必须使用类型转换,并且它可以在运行时产生ClassCastException。
现在,我们将使用java通用类重写如下所示的相同类。
package com.theitroad.generics; public class GenericsType<T> { private T t; public T get(){ return this.t; } public void set(T t1){ this.t=t1; } public static void main(String args[]){ GenericsType<String> type = new GenericsType<>(); type.set("hyman"); //valid GenericsType type1 = new GenericsType(); //raw type type1.set("hyman"); //valid type1.set(10); //valid and autoboxing support } }
注意main方法中GenericsType类的使用。
我们不需要进行类型转换,并且可以在运行时删除ClassCastException。
如果我们在创建时未提供该类型,则编译器将发出警告," GenericsType是原始类型。
泛型类型GenericsType <T>的引用应参数化"。
如果我们不提供类型,则该类型将变为"对象",因此它允许同时使用String和Integer对象。
但是,我们应该始终避免这种情况,因为在处理可能产生运行时错误的原始类型时,我们必须使用类型转换。
提示:我们可以使用`@SuppressWarnings(" rawtypes")"注释来禁止编译器警告,请查看Java注释教程。
还要注意,它支持Java自动装箱。
3. Java通用接口
可比接口是接口中泛型的一个很好的例子,它写为:
package java.lang; import java.util.*; public interface Comparable<T> { public int compareTo(T o); }
以类似的方式,我们可以在Java中创建通用接口。
我们还可以像在Map界面中那样具有多个类型参数。
同样,我们也可以为参数化类型提供参数化值,例如new HashMap <String,List <String >>();
是有效的。
4. Java通用类型
Java通用类型命名约定可帮助我们轻松理解代码,并且具有命名约定是Java编程语言的最佳实践之一。
因此,泛型也带有自己的命名约定。
通常,类型参数名称是单个大写字母,以使其易于与Java变量区分开。
最常用的类型参数名称为:
- E –元素(由Java Collections Framework广泛使用,例如ArrayList,Set等)
- K –键(在地图中使用)
- N –数字
- T –类型
- V –值(在地图中使用)
- S,U,V等–第二,第三,第四类型
5. Java通用方法
有时我们不希望整个类都被参数化,在这种情况下,我们可以创建java泛型方法。
由于构造函数是一种特殊的方法,因此我们也可以在构造函数中使用泛型类型。
这是一个显示Java泛型方法示例的类。
package com.theitroad.generics; public class GenericsMethods { //Java Generic Method public static <T> boolean isEqual(GenericsType<T> g1, GenericsType<T> g2){ return g1.get().equals(g2.get()); } public static void main(String args[]){ GenericsType<String> g1 = new GenericsType<>(); g1.set("hyman"); GenericsType<String> g2 = new GenericsType<>(); g2.set("hyman"); boolean isEqual = GenericsMethods.<String>isEqual(g1, g2); //above statement can be written simply as isEqual = GenericsMethods.isEqual(g1, g2); //This feature, known as type inference, allows you to invoke a generic method as an ordinary method, without specifying a type between angle brackets. //Compiler will infer the type that is needed } }
注意isEqual方法签名显示了在方法中使用泛型类型的语法。
另外,请注意如何在我们的Java程序中使用这些方法。
我们可以在调用这些方法时指定类型,也可以像普通方法一样调用它们。
Java编译器足够聪明,可以确定要使用的变量的类型,这种功能称为类型推断。
6. Java泛型绑定类型参数
假设我们想限制可以在参数化类型中使用的对象的类型,例如在比较两个对象的方法中,并且我们要确保接受的对象是可比较的。
要声明一个有界的类型参数,请列出类型参数的名称,然后列出extends关键字,再加上其上限,类似于下面的方法。
public static <T extends Comparable<T>> int compare(T t1, T t2){ return t1.compareTo(t2); }
这些方法的调用与无界方法类似,不同之处在于,如果我们尝试使用任何非Comparable的类,都会引发编译时错误。
绑定类型参数可以与方法以及类和接口一起使用。
Java泛型也支持多个范围,即<T扩展了A&B&C>。
在这种情况下,A可以是接口或者类。
如果A是类,则B和C应该是接口。
我们不能在多个范围内有多个类。
7. Java泛型和继承
我们知道,如果A是B的子类,则Java继承允许我们将变量A分配给另一个变量B。
因此,我们可能认为可以将A的任何泛型类型分配给B的泛型类型,但事实并非如此。
让我们用一个简单的程序来看一下。
package com.theitroad.generics; public class GenericsInheritance { public static void main(String[] args) { String str = "abc"; Object obj = new Object(); obj=str; //works because String is-a Object, inheritance in java MyClass<String> myClass1 = new MyClass<String>(); MyClass<Object> myClass2 = new MyClass<Object>(); //myClass2=myClass1; //compilation error since MyClass<String> is not a MyClass<Object> obj = myClass1; //MyClass<T> parent is Object } public static class MyClass<T>{} }
我们不允许将MyClass <String>变量分配给MyClass <Object>变量,因为它们不相关,实际上MyClass <T>的父对象是Object。
8. Java通用类和子类型
我们可以通过扩展或者实现来泛型一个通用类或者接口。
一个类或者接口的类型参数与另一类或者接口的类型参数之间的关系由extend和Implements子句确定。
例如,ArrayList <E>实现扩展Collection <E>的List <E>,因此ArrayList <String>是List <String>的子类型,而List <String>是Collection <String>的子类型。
只要我们不更改type参数,子类型关系就会保留,下面显示了多个type参数的示例。
interface MyList<E,T> extends List<E>{ }
List <String>的子类型可以是MyList <String,Object>,MyList <String,Integer>等。
9. Java通用通配符
问号(?)是泛型中的通配符,表示未知类型。
通配符可以用作参数,字段或者局部变量的类型,有时还可以用作返回类型。
在调用通用方法或者实例化通用类时,我们不能使用通配符。
在以下各节中,我们将学习上界通配符,下界通配符和通配符捕获。
9.1)Java泛型上界通配符
上限通配符用于放松方法中对变量类型的限制。
假设我们要编写一个将返回列表中数字总和的方法,那么我们的实现将是这样的。
public static double sum(List<Number> list){ double sum = 0; for(Number n : list){ sum += n.doubleValue(); } return sum; }
现在,上述实现的问题在于它不适用于List of Integers或者Doubles,因为我们知道List <Integer>和List <Double>不相关,这在使用上限通配符时很有用。
我们将通用通配符与extends关键字和上限类或者接口一起使用,这将允许我们传递上限或者其子类类型的参数。
可以像下面的程序一样修改上面的实现。
package com.theitroad.generics; import java.util.ArrayList; import java.util.List; public class GenericsWildcards { public static void main(String[] args) { List<Integer> ints = new ArrayList<>(); ints.add(3); ints.add(5); ints.add(10); double sum = sum(ints); System.out.println("Sum of ints="+sum); } public static double sum(List<? extends Number> list){ double sum = 0; for(Number n : list){ sum += n.doubleValue(); } return sum; } }
就像按照接口编写代码一样,在上述方法中,我们可以使用上限类Number的所有方法。
请注意,对于上界列表,除null之外,我们不允许将任何对象添加到列表中。
如果我们尝试在sum方法内将元素添加到列表中,则该程序将无法编译。
9.2)Java泛型无限制通配符
有时,我们希望通用方法适用于所有类型,在这种情况下,可以使用无界通配符。
与使用<?相同扩展Object>。
public static void printData(List<?> list){ for(Object obj : list){ System.out.print(obj + "::"); } }
我们可以为printData方法提供List <String>或者List <Integer>或者任何其他类型的Object列表参数。
与上限列表类似,我们不允许在列表中添加任何内容。
9.3)Java泛型下界通配符
假设我们要在方法中将整数添加到整数列表中,我们可以将参数类型保持为List <Integer>,但它将与Integers捆绑在一起,而List <Number>和List <Object>也可以容纳整数,因此我们可以使用下限通配符来实现。
我们使用带有super关键字和下限类的泛型通配符(?)来实现此目的。
我们可以传递下界或者下界的任何超类型作为参数,在这种情况下,java编译器允许将下界对象类型添加到列表中。
public static void addIntegers(List<? super Integer> list){ list.add(new Integer(50)); }
10.使用泛型通配符进行子类型化
List<? extends Integer> intList = new ArrayList<>(); List<? extends Number> numList = intList; //OK. List<? extends Integer> is a subtype of List<? extends Number>
11. Java泛型类型擦除
添加了Java泛型以在编译时提供类型检查,并且在运行时没有使用,因此Java编译器使用类型擦除功能来删除字节码中的所有泛型类型检查代码,并在必要时插入类型转换。
类型擦除可确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。
例如,如果我们有如下通用类;
public class Test<T extends Comparable<T>> { private T data; private Test<T> next; public Test(T d, Test<T> n) { this.data = d; this.next = n; } public T getData() { return this.data; } }
Java编译器用第一个绑定接口Comparable替换有界类型参数T,如下代码:
public class Test { private Comparable data; private Test next; public Node(Comparable d, Test n) { this.data = d; this.next = n; } public Comparable getData() { return data; } }
12.泛型常见问题解答
12.1)为什么我们在Java中使用泛型?
泛型提供了强大的编译时类型检查,并降低了ClassCastException和显式对象转换的风险。
12.2)泛型中的T是什么?
我们使用<T>创建通用类,接口和方法。
我们在使用T时将其替换为实际类型。
12.3)泛型如何在Java中工作?
通用代码确保类型安全。
编译器使用类型擦除在编译时删除所有类型参数,以减少运行时的重载。
13. Java泛型
泛型不支持子类型,因此
List <Number>number =new ArrayList <Integer>();
将无法编译,为什么泛型不支持子类型。我们无法创建通用数组,因此
List <Integer> [] array = new ArrayList <Integer> [10]
将无法编译,请阅读为什么我们无法创建通用数组?