浅谈Java中的泛型

引言:泛型一直是困扰自己的一个难题,但是泛型有时一个面试时老生常谈的问题;今天作者就通过查阅相关资料简单谈谈自己对泛型的理解;

一:什么是泛型:

  • 泛型就是参数化类型,就是所操作的数据类型被指定为一个参数,这种类型可以在类接口和方法中创建,分别称之为泛型类,泛型接口,泛型方法

二:为什么要使用泛型:

  • 使用泛型可以让我们编写的代码可以被很多类型的对象重用;
    例如我们不希望为String和file对象分别设计不同的类;实际上我们也并不需要这么做;
    就拿ArrayList类举例:可以聚集任何类型的对象;Javase5.0之前是使用继承实现的;ArrayList类只维护一个Object引用的数组;
Public class ArrayList{
    Public Object get(int i){
    }
  Public void add(Object o){
  }
  Public Object[] elementData;
  }

问题1:获取值必须进行强制类型转换;
问题2:没有错误检查可以向数组列表中添加任何参数;
针对以上两种情况泛型提出了更好的解决方案;类型参数;(type parameters)使我们设计的程序具有更好的可读性和安全性

三:泛型都有哪些?

  • 1:泛型类:

    就是一个或者多个具有类型变量的类;如下:
 public class Pair<T>{
    private T first;
    private T second;  
    public Pair(){
      first = null;
      second = null;
    }
    public void setFirst(T first){
      this.first =   first;
    }
    public void setSecond(T second){
      this.second = second
    }
    public T getFirst(){
        return this.first;
    }
    public T getSecond(){
        return Second;
    }
  }

Pair类引入了一个类型变量T,将其放在"<>"里面;并且将其放在类名的后面,泛型类也可以有多个类型变量;
可以将泛型类看做是普通类的工厂;

  • 2:泛型方法:

    带有类型参数的简单方法;
    泛型方法可以定义在普通类里面也可以定义在泛型类里面;
    所谓的泛型方法要么就是返回值是一个泛型,要么就是参数是泛型;
    class ArrayAlg{
      public static <T> T getMiddle(T[] a){
        return a[a.length/2]
      }
    }
    
  • 3:泛型接口

    就是一个或者多个具有类型变量的接口
    pulbic interface Collection<E> extends Iterable<E> {
    Iterator<E> iterator();
    }

四:泛型使用方法

  • 1:泛型代码和虚拟机

    注意虚拟机中没有泛型类型的对象,所有的对象都属于普通类;
    无论什么时候定义泛型,java虚拟机都会自动提供一个相应的原始类型(raw type);原始类型的名字就是删除泛型参数之后的泛型类型名;擦除类型变量,并替换为限定类型,如果没有限定类型的变量就替换成Object;
    如上面的Pair:替换之后的代码为:
 public class Pair{
    private Object first;
    private Object second;  
    public Pair(){
      first = null;
      second = null;
    }
    public void setFirst(Object first){
      this.first =   first;
    }
    public void setSecond(Object second){
      this.second = second
    }
    public Object getFirst(){
        return this.first;
    }
    public Object getSecond(){
        return Second;
    }
  }

如果泛型类使用了限定类型如下:

public class Interval<T extends Comparable & Serializabler>{
  public Interval(T first,T second){
    if(first.comparaTo(second)<=0){
      lower - first;upper = second;
    }
    ....
    private T lower;
    private T upper;
  }
}
//转换成原始类型之后
public class Interval extends erializabler{
  public Interval(Comparable first,Comparable second){
    if(first.comparaTo(second)<=0){
      lower - first;upper = second;
    }
    ....
    private Comparable lower;
    private Comparable upper;
  }
}
<1>:翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器就会插入强制类型转换;eg:
  Pair<Employee> buddies = ...;
  Employee buudy = buudies.getFirst();
  //这里擦书getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换。也就是说编译器把这个方法翻译为两条虚拟机指令:
  A:对原始方法Pair.getFirst的调用
  B:将返回的Object类型强制转换为Employee类型
<2>:翻译泛型方法
public static <T extends Comparable> T min(T[] a)是一个完整的方法族,擦除类型之后只剩下一个方法了:

 public static Comparable min(Comparable[] a)
 此时仅仅留下限定类型Comparable
eg: class DateInterval extends Pair<Date>{
         public void setSecond(Date second){
             if(second.compareTo(getFirst())>=0){
                 super.setSecond(second); 
             }
         }
     }
//类型擦出之后:
class DateInterval extends Pair{
         public void setSecond(Date second){
             if(second.compareTo(getFirst())>=0){
                 super.setSecond(second); 
             }
         }
     }

此时存在另一个从Pair中继承来的setSecond方法,即
public void setSecond(Object second)
这个方法显然和setSecond(Date date)不是同一个方法;
考虑下面的代码:

DateInterval interval = new DateInterval(...);
 Pair<Date> pair = interval;
 pair.setSecond(aDate);

这里我们希望setSecond的调用具有多态性;并调用最合适的那个方法;希望pair调用DateInterval.setSecond方法;但此时类型擦书和多态已经发生了冲突:想要解决这个问题我们需要建立一个桥方法;

public void setSecond(Object second){
  setSecond((Date) second)
}

总之:记住有关java泛型转换的事实:
A:虚拟机中没有泛型,只有普通方法和普通类;
B:所有的类型参数都用他们的限定类型替换
C:桥方法被合成用来保持多态;
D:为保持类型安全性,必要时插入强制类型转换。

2:约束和局限性:

使用java中的泛型时需要考虑一些限制,大多是限制都是由于类型擦除引起的;

<1>:不能用基本类型实例化类型参数
没有Pair<double>只有Pair<Double>
<2>:运行时类查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此所有的类型查询只产生原始类型。例如:
if(a instanceof Pair<String>)//ERROR
<3>:不能创建参数化类型的数组:

Pair<String>[] table = new Pair<String>[10]//ERROR;
但是声明类型Pair<String>[]的变量仍是合法的;

<4>:Varargs警告:

由于java布置池泛型类型的数组;当我们向一个参数个数可变的方法 传递一个泛型类型的实例:
eg:

public static <T> void addAll(Collection<T> coll,T...ts){
  for(T t:ts)coll.add(t);
}
实际上ts是一个数组,包含提供的所有实参。
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ....;
Pair<String> pair2 = ....;
Pair<String> pair3 = ....;
addAll(table,pair1,pair2);
为了调用这个方法,Java虚拟机必须建立一个Pair<String>数组;违反前面的规定;但是这种情况并不会报告错误仅仅会报告一个警告可以通过两种方式去消除它:
A:为addAll方法添加标注@SuppressWarning("unchecked").
B:@SafeVarargs直接标志addAll方法
@SafeVarags
public static <T> void addAll(Collection<T> coll,T...ts)
#####<5>:不能实例化类型参数
new T(...),new T[]或者T.class都是不允许的;
但是可以通过反射加上一定的API便可实现泛型的实例化;
```java
  public static <T> Pair<T> makePair(Class<T> cl){
      try{
          return new Pair<>(cl.newIntance(),cl.newInstance())
      }catch(Exception e){
        return null;
      }
  }
Pair<String> p = Pair.makePair(String.class)

类型擦除会让这个方法永远构造Object[2]数组;
如果数组仅仅作为一个类的私有实例域,就可以将这个数组声明为Object[],并在获取元素时进行类型转换。例如ArrayList就是这样实现的;

Public class ArrayList<E>{
  Private Object[] elements;
  @SuppressWarning(‘unchecked’)
  public E get(int n){
      return (E) elements[n]}
   }
  Public void set(int n,E e){elements[n] = e};
  Public class ArrayList<E>{
       Private E[] elements;
       Public ArrayList(){
       Elements = (E[])new Object[10];//假像:类型擦除会使其无法差距
  }
}

编译时不会报错,但是当我们的程序执行时,当我们把Object[]引用赋值给T[]时就会报错;将会发生ClassCastException异常;
这种情况下我们可以利用反射:

Public static <T extends Comparable> T[] minmax(T…a){
  T[] mm = (T[])Array.newInstance(a.getClass().getComponentType(),2)
}
<6>泛型类的静态上下文中类型变量无效

不能在静态域或方法中引用类型变量(即泛型);(即被static修饰的域或者方法)

<7>:不能抛出或者捕获泛型类的实例

既不能抛出也不能捕获泛型类的实例对象。实际上,甚至泛型类扩展Throwable都是不合法的;T extends Exception(Throwable)
Catch字句中不能使用类型变量;catch(T e)Error;
PS:可以消除已检查异常的检查:
Java异常处理的一个基本原则:必须为所有的已检查异常提供一个处理器;不过可以利用泛型消除这个限制;
当你必须捕获run中所有的已检查异常,将其包装到未检查异常中,因为run方法声明为不抛出任何已检查异常;
不过在这里我么你没有选择这种“包装”我们只是抛出异常,并“哄骗浏览器”让它认为这不会一个已检查异常;
通过使用泛型类,擦除,和@SuppertWarning标注就可以消除java类型系统的部分限制

<8>注意擦除后的冲突:

当泛型类型被擦除时,无法创建引发冲突的条件,如:
在某个泛型类中添加equals()方法,当我们给这个泛型类中间穿具体的类型时:
进过类型擦除之后equals(String o)方法变为equals(Object o);
所以此时从概念上讲:他有两个equals方法:
Boolean equals(String)
Boolean equals(Object)和Object.Equals(Object)方法冲突;
补救方法就是重命名引发错误的方法
另外一个原则:“想要支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类”而这两个接口是同一个接口的不同参数化;
例如下面的代码就是非法的

Class Calendar implements Comparable<Calendar>{…}
Class GregorianCalendar extends Calendar implements Comparable< GregorianCalendar>(Error)
GregorianCalendar会事先Comparable<Calendar>和Comparable<GregorianCalendar>

这是同一接口的不同参数化;这一限制和类型擦柱的关系不是十分明确;下列的非泛型的版本就是合法的;
原因:有可能与合成的桥方法产生冲突。实现了Comparable<x>的类可以获得一个桥方法;

Public int comparaTo(Object other){
  return compareTo(X) other
};

对于不同类型的;不能有两个这样的方法;

<9>泛型类型的继承规则:
  • 例如:Employee和Manager。Pair<Manager>和Pair<Employee>之间没有任何关系;
    无论S和T有什么关系,通常Pair<S>和Pair<T>之间都不会有什么联系;
    继承泛型类
  • 子类不是泛型类:需要给父类传递类型常量

    当给父类传递的类型常量为String时,那么在父类中所有T都会被String替换!

  • 子类是泛型类:可以给父类传递类型常量,也可以传递类型变量

3:通配符类型:

为了解决固定的泛型类型使用的不便利:java的设计者们发明了一种巧妙的(仍然是安全的)“解决方案”:通配符类型;

<1>:子类通配符:
   例如:Pair<? Extends Employee>
   表示任何泛型Pair类型,它的类型参数是Employee的子类;如Pair<Manager>

例子:
Pair<Manager> managerBuddies = new
Pair<>(ceo,cfo);
Pair<? Extends employee>
wildcardBuddies = managerBuddies;
wildcardBuddies.setFirst(lowlyEmployee);//error;
这可能不会以你破坏,对setFirst的调用有一个类型错误;
当你调用setFirst(? Extends Employee)编译器仅仅知道是某个Employee的子类型,但是不知道具体是什么类型;它拒绝传递任何的类型。
但是使用getFirst就不存在这个问题;
这就是引入限定的统配符的关键之处;

<2>:通配符的超类型限定:
   通配符限定和类型变量限定十分类似;但是还有一个附加能力,即可以指定一个超类型限定(supertype bound)
   ? super Manager设置类型的下限;
   这个统配符限制为Manager的所有超类型;带有超类型限定的通配符的行为和12.8接讲的正好相反,可以为方法提供参数,但是不能使用返回值。
例如:
Pair<? Super Manager>
Void setFirst(? Super Manager)
? super Manager getFirst();

编译器不知道setFirst方法的确切类型,但是可以调用任意Manager对象(或其子类型,例如Executive)调用它,而不能使用Employee对象调用它,然而,如果调用getFirst,返回的对象类型就不能的都保证;只能将它的值赋给Object;
直观的讲,带有超类型的限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象中读取;

public void set(List<? extends Number> list){
        //list.add(new Integer(1));
        //可以获取泛型参数类型的值,但是不能向里面添加值
        Number i =list.get(0);
        
    }
    public void set1(List<? super Integer> list){
        list.add(new Integer(1));
        //不可以获取泛型参数类型的值,但是能向里面添加值
        //Integer i = list.get(0);
    }

超类型限定的另外一种应用:Comparable接口本身就是一个泛型类型;

Public interface Comparable<T>{
       Public intcompareTo(T other);
}

当有一个具体的类型传入其中时,相应的泛型类型就会被自动转换成传入的类型;se5.0之前。Other是一个Object并且这个方法的实现需要强制类型转换;

<3>:无限定的通配符:

还可以使用无限定的通配符;例如Pair<?>.

4:泛型和反射
<1>:使用Class<T>参数进行类型匹配
Public static <T> Pair<T> makePair(Class<T> c) throws
InstantiationException,IllegalAccessException{
Return new Pair<>(c.newInstance(),c.newInstance());
}

调用:makePair(Emloyee.class)

<2>虚拟机中的泛型类型信息:
   Java泛型的卓越特性之一就是在虚拟机中的类型擦除;虽然擦除了,但是在类中仍然会保留一些泛型祖先的微弱记忆;
   在java虚拟机中,需要重新构造是实现者声明的泛型类,以及方法中的所有内容;(通过反射去获得用户传递的类型的一些信息)不会知道特定对象或者方法调用,以及如何解释类型采参数;

为了表达泛型类型的声明,java.lang.reflect包中提供了一个新的接口Type,这个接口包含了下列子类型的声明:
Class类,描述具体的类型
TypeVariable接口
描述类型变量(如T extends Comparable<? Super T>)
WildcardType接口
描述通配符(如?Super T)
parameterizedType接口,描述泛型类或者接口类型(如Comparable <? Super T>)
GenericArrayType接口,描述泛型数组(如T[]);

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容