目录
1.为什么引入泛型
1.重复逻辑的例子(该示例来自于博客:https://blog.csdn.net/qq_27093465/article/details/73229016)
public class IntegerPoint {
private Integer x; // 表示X坐标
private Integer y; // 表示Y坐标
public void setX(Integer x) {
this.x = x;
}
public void setY(Integer y) {
this.y = y;
}
public Integer getX() {
return this.x;
}
public Integer getY() {
return this.y;
}
}
public class FloatPoint {
private Float x; // 表示X坐标
private Float y; // 表示Y坐标
public void setX(Float x) {
this.x = x;
}
public void setY(Float y) {
this.y = y;
}
public Float getX() {
return this.x;
}
public Float getY() {
return this.y;
}
}
为了减少重复性代码,尝试使用继承来实现该功能,从而提高代码复用率,减少重复代码:
public class ObjectPoint {
private Object x;
private Object y;
public void setX(Object x) {
this.x = x;
}
public void setY(Object y) {
this.y = y;
}
public Object getX() {
return this.x;
}
public Object getY() {
return this.y;
}
}
public class IntegerPoint extends ObjectPoint{
}
public class FloatPoint extends ObjectPoint {
}
由于IntegerPoint类和FloatPoint类都继承ObjectPoint类,继承其get和set方法,可以避免在两个类中出现重复代码。但是,这样设计有一个问题:
public static void main(String[] args) {
ObjectPoint point1 = new IntegerPoint();
point1.setX(11.11);
point1.setY(22.22);
ObjectPoint point2 = new FloatPoint();
point2.setX(new Integer(11));
point2.setY(new Integer(22));
Integer x = (Integer)point1.getX() + (Integer)point2.getX();
System.out.println(x);
}
这种写法,通过强制类型转换,在子类之间进行了转换,由于set方法的参数是Object类型,是父类型,在get时获得的也是object类型,编译器不知道其引用的究竟是哪一个子类型,因此在进行强制类型转换的时候,不会编译错误。但在运行时就会报java.lang.ClassCastException错,从而运行失败。
2.集合的例子
public static void main(String[] args) {
List list = new ArrayList();
list.add("zhangSan");
list.add("男");
list.add(100);
for (int i = 0; i < list.size(); i++) {
String str = (String) list.get(i);
System.out.println("str:" + str);
}
}
从上面这个集合的例子,可以看到:ArrayList中可以存放各种类型的对象,因此放入两个字符串和一个整数是可以编译通过且正常运行的。但是当对集合中的每个对象进行操作时,就容易出现对整型对象使用字符串类型的方法的情形,导致运行时报java.lang.ClassCastException错,运行失败。
2.什么是泛型
泛型,即“参数化类型”。顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。该解释来源于博客:https://www.cnblogs.com/ljxe/p/5521840.html
泛型类:具有一个或多个泛型变量的类。在类的声明处加入<T>,该类就成为一个泛型类。其中的字母可以任意取,但一般情况用T、U。(E表示Element、K-V表示键值对。约定俗成,非强制规定)
public class Pair<T> {
private T first;
private T second;
public Pair() {
first = null;
second = null;
}
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
泛型方法:可以定义在普通类中,也可以定义在泛型类中
class ArrayAlg {
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
}
// 调用方法
String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");
类型变量的限定:使用extends关键字限定泛型类型,多个限定通过 & 分隔。可以通过接口限定类型,也可以通过类限定。类需放在所有接口的最前面。
public static <T extends Comparable & Serializable> T min(T[] a) {
// T 的实际类型必须同时实现了Comparable和Serializable接口
}
3. 泛型的使用
- 泛型类
通过泛型类的方式实现坐标的例子:
public class Point<T> {
private T x;
private T y;
public void setX(T x) {// 作为参数
this.x = x;
}
public void setY(T y) {
this.y = y;
}
public T getX() {// 作为返回值
return this.x;
}
public T getY() {
return this.y;
}
}
通过泛型定义了坐标类型,在实例化整型坐标和浮点型坐标时指定类型,中间对对象赋值操作不正确时编译器检查会不通过。
在ArrayList声明时指定String类型,在通过add方法向链表中添加元素时,编译器会对元素类型进行检查,非String类型的元素不能加入到链表中,编译不通过。同样,通过get方法访问链表元素时,也无需进行强制类型转换。
在Java1.5时,List、Map等集合类型引入了泛型机制,但为了向下兼容,保留了原始类型,即可以不指定泛型类型。
- 泛型接口
泛型接口与泛型类基本相同,例如:Iterator接口
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
}
- 泛型方法
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。泛型方法与泛型类是相对独立的。
方法的声明由:修饰符 返回值类型 方法名 参数列表组成,而泛型方法在修饰符和返回值类型之间多了一部分<T>,可以理解为泛型方法的独特声明。因此,泛型类中定义的成员变量不一定就是泛型方法,泛型方法同样也不一定要在泛型类中 声明。
静态方法:静态方法不能直接访问泛型类的泛型变量
如图所示,泛型类中定义静态方法,该静态方法参数为泛型类的类型变量,编译不通过。如果静态方法需要使用泛型变量,则该静态方法需要声明为泛型方法
4. 泛型的优势
-
可读性:泛型中,对于具体的类使用,可以清晰看出该类的类型
-
安全性:编译器对其进行检查,避免或减少运行时出现的强制类型转换的错误
-
可重用性:类型的参数化,编写的代码可以被很多不同类型的对象所重用,避免重复逻辑或重复代码,提高代码质量,也提高代码可读性
5. 泛型实现机制-类型擦除
- 虚拟机没有泛型类型对象,所有对象都属于普通类
public static void main(String[] args) {
List<String> names = new ArrayList<>();
List<Integer> ages = new ArrayList<>();
List<Number> weights = new ArrayList<>();
System.out.println("names class:" + names.getClass());
System.out.println("ages class:" + ages.getClass());
System.out.println("weights class:" + weights.getClass());
}
运行结果为:
names class:class java.util.ArrayList
ages class:class java.util.ArrayList
weights class:class java.util.ArrayList
可以看出,在虚拟机中,不区分ArrayList的参数类型,统统为ArrayList类型。因此,泛型仅仅只在编译阶段有效,编译通过之后,虚拟机会将类型参数擦除,也就是说泛型信息不会进入运行时阶段。(强制类型转换也是存在的)
上述代码中,Number是Integer的父类,试想:List<Number> numberList1 = ages; 该语句是否成立呢?
答案是否定的,编译不通过:Type mismatch: cannot convert from List<Integer> to List<Number>。 虽然Integer是Number的子类,但List<Integer>和List<Number>之间没有关系。(如果希望二者之间有逻辑上的继承关系,可以使用通配符?帮助实现。如:List< ? extends Number > numberList2 = ages)
public class Test {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
List<Integer> ages = new ArrayList<>();
List<Number> weights = new ArrayList<>();
// List<Number> numberList1 = ages; //编译不通过,报错
List< ? extends Number > numberList2 = ages;
Test test = new Test();
ages.add(new Integer(11));
// System.out.println(test.fun(ages)); //编译不通过,报错
System.out.println(test.fun1(ages));
}
public Number fun(List<Number> list) {
return list.get(0);
}
public Number fun1(List<? extends Number> list) {
return list.get(0);
}
}
- 类型擦除:擦除类型变量,替换为限定类型
- 使用第一个限定的类型变量来替换(可以通过接口限定类型,也可以通过类限定。类需放在所有接口的最前面。限定顺序是重要的)
public class Pair<T extends Comparable & Serializable> { private T first; public T getFirst() { return first; } public void setFirst(T first) { this.first = first; } } //类型擦除后 public class Pair { private Comparable first; public Comparable getFirst() { return first; } public void setFirst(Comparable first) { this.first = first; } }
-
如果没有限定,则使用Object类型替换
public class Pair<T> { private T first; public T getFirst() { return first; } public void setFirst(T first) { this.first = first; } } //类型擦除后 public class Pair { private Object first; public Object getFirst() { return first; } public void setFirst(Object first) { this.first = first; } }
- 翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器自动进行强制类型转换
如下图: 链表在声明时指定String类型,add时只能添加String类型对象,取出元素时不需要进行强制类型转换,编译器自动插入强制类型转换
- 翻译泛型方法
public static <T extends Comparable> T min(T[] a)
//擦除类型后
public static Comparable min(Comparable[] a)
class DateInterval extends Pair<LocalDate>
{
public void setFirst(LocalDate first) {
... ...
}
}
//擦除类型后
class DateInterval extends Pair
{
public void setFirst(LocalDate first) {
... ...
}
}
//由于继承Pair,DateInterval还存在一个 public void setFirst(Object first) 方法
当定义如下语句时:
DateInterval interval = new DateInterval(...);
Pair<LocalDate> pair = interval; //子类对象即父类对象, OK
pair.setFirst(aDate);
由于pair引用的是DateInterval类型的对象,因此最为正确的是调用DateInterval类的setFirst方法。
但由于类型擦除了,父类中方法变成了setFirst(Object object),DateInterval中的setFirst(LocalDate first)方法并没有覆盖掉父类中的方法,所以DateInterval同时存在两个setFirst方法。
pair对象声明为Pair<LocalDate>类型的,该类型只有setFirst(Object object)方法。虚拟机通过pair引用的DateInterval类型的对象去调用这个方法,因此会调用DateInterval类的setFirst(Object object)方法,这个方法是编译器生成的一个“桥方法”:
class DateInterval {
public void setFirst(Object object) {
setFirst((Date)object);
}
}
6.VS C++模板
-
关键字:Java泛型没有特殊关键字标识,C++使用Template关键字标识
-
C++模板可以使用基本数据类型,Java 泛型不能接受基本类型作为类型参数――它只能接受引用类型。这意味着可以定义 List<Integer>,但是不可以定义 List<int>
-
C++
-
在Java中,不管类型参数是什么,泛型类的所有实例都是同一类型类型参数会在运行时被擦除。而C++中,参数类型不同,实例类型也不同。在c++中存在为每个模板的实例化产生不同的类型,这一现象被称为“模板代码膨胀”
-
Java中,泛型类的类型参数变量 T 不能用于静态方法和静态变量,因为他们会被MyClass<Foo>和MyClass<Bar>共享。但在C++中,这些类是不同的,类型参数可以用于静态方法和静态变量。
-
在java中,尖括号通常放在方法名前,而c++则是放在方法名后,c++的方式容易产生歧义,例如g(f<a,b>(c)),这个则有两种解释,一种是f的泛型调用,c为参数,a,b为泛型参数。另一种解释,则是,g调用,两个bool类型的参数。