Java泛型:泛型的定义(类接口对象)使用继承

Posted mrzhang1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java泛型:泛型的定义(类接口对象)使用继承相关的知识,希望对你有一定的参考价值。

地址   http://blog.csdn.net/lirx_tech/article/details/51570138

1. 设计泛型的初衷:

    1) 主要是为了解决Java容器无法记忆元素类型的问题:

         i. 由于Java设计之初并不知道会往容器中存放什么类型的元素,因此元素类型都设定为Object,这样就什么东西都能放了!

         ii. 但是这样设计有明显的缺点:

             a. 取出元素的时候必须进行强制类型转换(尽管集合在运行时里面元素的“运行时类型”不变,即元素的getClass返回的还是最初自己的类型而不是Object);

             b. 如果不小心往集合里加了不相同类型的元素可能会导致类型异常(进行equals、compare比较的时候尤为明显);

             c. 由于没有类型就需要在很多地方进行强制类型转换,但是这样做增加了编程的复杂度,并且代码也不美观(臃肿),维护起来也更加困难;

    2) 泛型的概念定义:

         i. 从Java 5开始,引入了参数化类型(Parameterized Type)的概念,改造了所有的Java集合,使之都实现泛型,允许程序在创建集合时就可以指定集合元素的类型,比如List<String>就表名这是一个只能存放String类型的List;

         ii. 泛型(Generic):就是指参数化类型,上面的List<String>就是参数化类型,因此就是泛型,而String就是该List<String>泛型的类型参数;

    3) 泛型的好处:

         i. 使集合可以记住元素类型,即取出元素的时候无需进行强制类型转化了,可以直接用原类型的引用接收;

         ii. 一旦指定了性参数那么集合中元素的类型就确定了,不能添加其他类型的元素,否则会直接编译保存,这就可以避免了“不小心放入其他类型元素”的可能;

         iii. 上述保证了如果在编译时没有发出警告,则在运行时就一定不会产生类型转化异常(ClassCastException);

         iv. 显然,泛型使编程更加通用,并且代码也更加简洁,代码更加容易维护;

 

2. 创建泛型对象——自动类型推断的菱形语法:

    1) 首先,定义泛型引用一定要使用尖括号指定类型参数,例如:List<String> list、Map<String, Integer>等,其中的String、Integer之类的就是类型参数;

    2) 其次,使用构造器构造泛型对象的时候可以指定类型参数也可以不指定,例如:

         i. List<String> list = new List<String>();  // 这当然是对的

         ii. List<String> list = new List<>();  // 这样对,因为List的类型参数可以从引用推断出!

!!但是引用的类型参数是一定要加的,否则无法推断;

    3) 由于<>很像菱形,因此上面的语法也叫做菱形语法;

    4) 错误提示:引用无类型参数但构造器有类型参数的写法是不对的!例如,List list = new List<String>();

!!至于为什么不对,这会在泛型原理的章节中详细介绍,这里先记住这样写不对就行了!

!反正就是一个原则,泛型引用是一定要指定类型参数的!!

    5) 示例:

 

[java] view plain copy
 
  1. public class Test {  
  2.       
  3.     public static void main(String[] args) {  
  4.         ArrayList<String> list = new ArrayList<>();  
  5.         list.add("lala");  
  6.         list.add("haha");  
  7.         // list.add(5); // 类型不符,直接报错!!  
  8.         list.forEach(ele -> System.out.println(ele)); // 可以看到取出的ele无需强制类型转换,直接就是String类型的  
  9.         // 说明泛型集合能记住元素的类型,代码简洁了很多  
  10.           
  11.         HashMap<String, Integer> map = new HashMap<>();  
  12.         map.put("abc", 15);  
  13.         map.put("def", 88);  
  14.         map.forEach((key, value) -> System.out.println(key + " : " + value)); // 可以看到key、value同样无需强制类型转化  
  15.     }  
  16. }  

 

 

3. 定义泛型类、接口:

    1) 不仅Java的集合都定义成了泛型,用户自己也可以定义任意泛型的类、接口,只要在定义它们时用<>来指定类型参数即可;

    2) 例如:public class Fruit<T> { ... },其中<T>指定了该泛型的类型参数,这个T是一个类型参数名,用户可以任意命名(就像方法参数的形参名一样),只有在定义该泛型的对象时将T替换成指定的具体类型从而产生一个实例化的泛型对象,例如:Fruit<String> fruit = new Fruit<>(...);

    3) 类型形参可以在整个接口、类体内当成普通类型使用,集合所有可使用普通类型的地方都可以使用类型形参,例如:

 

[java] view plain copy
 
  1. public interface MyGneric<E> {  
  2.     E add(E val);  
  3.     Set<E> makeSet();  
  4.     ...  
  5. }  

!!可以看到,在接口内/类体内甚至还可以使用该类型形参运用泛型!例如上面makeSet方法返回一个泛型Set;

 

    4) 定义泛型构造器:泛型的构造器还是类名本身,不用使用菱形语法,例如

 

[java] view plain copy
 
  1. public class MyGenric<T> {  
  2.     MyGeneric(...) { ... }  
  3.     ...  
  4. }  

!定义构造器无需MyGeneric<T>(...) { ... }了,只有在new的时候需要用到菱形语法;

 

4. 实现/继承泛型接口/泛型类:

    1) 定义泛型和使用泛型的概念:主要区别就是定义和使用

         i. 那Java的方法做类比,Java的方法在定义的时候使用的都是形参(虚拟参数),但是在调用方法(使用方法)的时候必须传入实参;

         ii. 同样泛型也有这个特点,泛型的类型参数和方法的参数一样,也是一种参数,只不过是一种特殊的参数,用来表示未知的类型罢了;

         iii. 因此,泛型也是在定义的时候必须使用形参(虚拟参数,用户自己随意命名),但是在使用泛型的时候(比如定义泛型引用、继承泛型)就必须使用实参,而泛型的实参就是具体的类型,像String、Integer等具体的类型(当然也可以是自定义类型);

    2) 泛型定义的时候使用形参,例如:public class MyGeneric<T> { ... }  // T就是一个自己随意命名的类型形参

    3) 使用泛型的时候必须传入实参:

         i. 定义引用(对象)的时候毫无疑问,肯定需要传实参:ArrayList<String> list = ...;   // 必须用具体的类型,像这里就是String来代替形参,即实参

         ii. 实现/继承一个泛型接口/类的时候:

!!你在实现/继承一个接口/类的时候实际上是在使用该接口/类,比如:public class Son extends Father { ... }中Father这个类就是正在被使用,毫无疑问,必定是在使用;

!!因此泛型其实无法继承/实现,因为在实现/继承的时候必须为泛型传入类型实参,给定实参后它就是一个具体的类型了,就不再是泛型了

!!示例:public class MyType extends MyGeneric<String> { ... } // implements、extends的时候必须传入类型实参,因为实在使用泛型!!

!!原则上,任何编程语言都不允许泛型模板层层继承!!

    4) 继承之后,父类/接口中的所有方法中的类型参数都将变成具体的类型,你在子类中覆盖这些方法的时候一定要用具体的类型,不能继续使用泛型的类型形参了,例如:

 

[java] view plain copy
 
  1. class Father<T> {  
  2.     T info;  
  3.     public Father(T info) {  
  4.         this.info = info;  
  5.     }  
  6.     public T get() {  
  7.         return info;  
  8.     }  
  9.     public T set(T info) {  
  10.         T oldInfo = this.info;  
  11.         this.info = info;  
  12.         return this.info;  
  13.     }  
  14. }  
  15.   
  16. class Son extends Father<String> { // 所有从父类继承来的方法的类型参数全部都确定为String了  
  17.     // 因此在覆盖的时候都要使用具体的类型实参了!  
  18.       
  19.     public Son(String info) {  
  20.         super(info);  
  21.     }  
  22.   
  23.     @Override  
  24.     public String get() {  
  25.         return "haha";  
  26.     }  
  27.   
  28.     @Override  
  29.     public String set(String info) {  
  30.         return "lala";  
  31.     }  
  32.       
  33. }  

!!这一定能保证,这三个方法都是从父类中继承来的,只不过类型形参T被实例化成了String;

 

5. 泛型参数继承:

    1) 上面派生出来的类不是泛型,是一个实体类型,因为其继承的泛型是具有类型实参的,而Java还支持一种特殊的语法,可以让你从泛型继续派生出泛型,而泛型的类型参数可以继续传承下去;

    2) 语法如下:

 

[java] view plain copy
 
  1. class Father<T> { ... }  
  2.   
  3. class Son<T> extends Father<T> { ... }  

!即子泛型可以传承父泛型的泛型参数,那么在子类中泛型参数T就和父类的完全相同,还是照常使用(和父类一样正常使用);

 

    3) 注意:

         i. 这里extends Father<T>了,因此父类泛型Father就是被使用了,而按照之前讲的规则,使用给一个泛型是必须要指定类型实参的!因此这里的这个语法是一种特殊语法,Java专门为这种语法开了后门,这种语法只有在类型参数传承的时候才会用到(即上面这种应用);

         ii. 一旦使用了这种语法,就表示要进行类型参数的传承了(即父类的T传递给子类继续使用,因此子类也是一个跟父类一样的泛型);

         iii. 并且一旦使用了这种语法,那么子类定义中的Son<T>和extends Father<T>中的类型参数必须和定义父类时的类型参数名完全一样!!

              a. 以下三种情况全部错误(全部发生编译报错):

 

[java] view plain copy
 
  1. class Father<T> { }  
  2. class Son<E> extends Father<T> { }  
  3.   
  4. class Father<T> { }  
  5. class Son<T> extends Father<E> { }  
  6.   
  7. class Father<T> { }  
  8. class Son<E> extends Father<E> { }  

!!必须全部使用和父类定义相同的类型参数名(T)!才行,这是Java语法的特殊规定;

 

    4) 其实Java容器中很多类/接口都是通过类型参数传承来定义的:

         i. 最典型的例子就是:public interface List<T> extends Collection<T> { ... }

         ii. 虽然"如果A是B的父类,但Generic<A>不是Generic<B>"的父类,但"如果A是B的父类,那A<T>一定是B<T>的父类"!这是一定的;

         iii. 因为类型参数传承的定义方式本身就是:Son<T> extends Father<T>,那Father<T>一定是Son<T>的父类咯!

 

 

6. 在使用泛型的时候可以不使用菱形语法指定实参,直接裸用类型名:

    1) 例如:

         i. 定义引用(对象)时裸用类名:ArrayList list = new ArrayList(); // 当然也可以写成ArrayList list = new ArrayList<>();

         ii. 实现/继承:public class MyType extends MyGeneric { ... }

!!上面使用的类型或者接口在定义的时候都是泛型!!但是使用它们的时候忽略类型参数(都不用加菱形);

    2) Java规定,一个泛型无论如何、在任何地方、不管如何使用,它永远都是泛型,因此这里既是你忽略类型实参它底层也是一个泛型,那么它的类型实参会是什么呢?既然我们没有显式指定,那么Java肯定会隐式为其指定一个类型实参吧?

    3) 答案是肯定的,如果使用泛型的时候不指定类型实参,那么Java就会用该泛型的“原生类型“来作为类型实参传入!

!!那么“原生类型“是什么呢?这里先不介绍,会在下一章的”泛型原理“里详细分解;

!!但是我们这里可以先透露一下,Java集合的原生类型基本都是Object,因此像上面的ArrayList list = new ArrayList();写法其实传入的是Object类型实参,即ArrayList<Object>!



以上是关于Java泛型:泛型的定义(类接口对象)使用继承的主要内容,如果未能解决你的问题,请参考以下文章

java中接口具有泛型,继承的时候该怎么处理

Java泛型简介定义和使用含有泛型的类定义和使用含有泛型的方法定义和使用含有泛型的接口泛型通配符?受限泛型

Java自用高级编程-5.泛型

Java泛型学习笔记 - 泛型的继承

java学习----泛型

泛型的小千世界(java)