Java泛型详解

简介: Java泛型详解

【1】概述

为什么要有泛型(Generic)?

1.解决元素存储的安全性问题
2.解决获取数据元素时,需要类型强转的问题

泛型,JDK1.5新加入的,解决数据类型的安全性问题,其主要原理是在类声明时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这样在类声明或实例化时只要指定好需要的具体的类型即可。


Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮。


在引入范型之前,Java类型分为原始类型、复杂类型,其中复杂类型分为数组和类。引入范型后,一个复杂类型就可以在细分成更多的类型。例如原先的类型List,现在在细分成List<Object>, List<String>等更多的类型。


注意,现在List<Object>, List<String>是两种不同的类型,他们之间没有继承关系,即使String继承了Object。因此下面的代码是非法的:

List<String> ls = new ArrayList<String>();
List<Object> lo = new ArrayList<Object>();
lo=ls;//error 泛型不同的引用不能相互赋值。
System.out.println(ls.getClass()==lo.getClass());//true

在引入范型之前,要在类中的方法支持多个数据类型,就需要对方法进行重载,在引入范型后,可以解决此问题(多态),更进一步可以定义多个参数以及返回值之间的关系。

例如下面代码与泛型版本为:

//方法重载
public void write(Integer i, Integer[] ia);
public void write(Double  d, Double[] da);
//泛型版本
public <T> void write(T t, T[] ta);

【2】定义&使用

类型参数的声明定义可以使用 T S E等来表示。


类型参数名可以使用任意字符串,建议使用有代表意义的单个字符,以便于和普通类型名区分,如:T代表type,有源数据和目的数据就用S,D,子元素类型用E等。当然,你也可以定义为XYZ,甚至xyZ。


实例1:集合泛型符号E

public interface Collection<E> extends Iterable<E> {
  //...
}


实例2:自定义泛型接口

public interface MyGeneric<T> {
    T myMethod(T t);
}

【2.1】 定义带类型参数的类–泛型类


在定义带类型参数的类时,在紧跟类名之后的<>内,指定一个或多个类型参数的名字,同时也可以对类型参数的取值范围进行限定,多个类型参数之间用逗号分隔。


定义完类型参数后,可以在定义位置之后的类的几乎任意地方(静态块,静态属性,静态方法除外)使用类型参数,就像使用普通的类型一样。


如果泛型类是一个接口或抽象类,则不可创建泛型 类的对象。另外,不能在catch中使用泛型。


实例3:泛型类实现泛型接口

public class MyGenericClass<E> implements MyGeneric<E> {
    @Override
    public E myMethod(E e) {
        System.out.println(e);
        return e;
    }
    public static void main(String[] args){
      // 这里限定了类型
        MyGenericClass<Integer> myGenericClass = new MyGenericClass<Integer>();
        myGenericClass.myMethod(1);
        myGenericClass.myMethod(1L);//这里将会出错
    }
}

如下所示则可以传入任何类型:

public static void main(String[] args){
   // 注意这里
       MyGenericClass myGenericClass = new MyGenericClass<Integer>();
       myGenericClass.myMethod(1);
       myGenericClass.myMethod(1L);
       //可以传入任何类型,not only integer
   }

【2.2】 定义带类型参数方法–泛型方法


在定义带类型参数的方法时,在紧跟可见范围修饰(例如public)之后的<>内,指定一个或多个类型参数的名字,同时也可以对类型参数的取值范围进行限定,多个类型参数之间用,号分隔。

定义完类型参数后,可以在定义位置之后的方法的任意地方使用类型参数,就像使用普通的类型一样。


语法格式如下:

[访问权限]  <泛型>  返回类型  方法名([泛型标识 参数名称])  抛出的异常

示例如下:

// 泛型方法
 public <T, S extends T> T testGenericMethodDefine(T t, S s){
     //...
 }
# 或
// 泛型类的成员方法
public  T testGenericMethodDefine2(T t){
     //...
 }

第一个方法说明为泛型方法,类型可与泛型类类型不同;第二个方法表明类型T必须与泛型类一致(可以理解为泛型类的成员方法)!


实例如下:

  public  <T> T getE(String j,Double i){  
        return (T) (i+j);  
    }  
    //返回为S类型  
    public  <T,S extends T> T getE(T j,S i){  
        return i;  
    }  


【2.3】类型参数的限定


如果限制只有特定某些类可以传入T参数,那么可以对T进行限定,如:只有实现了特定接口的类:<T extends Comparable>,表示的是Comparable及其子类型。


为什么是extends不是 implements,或者其他限定符?


这是一个约定,严格来讲,该表达式意味着:T subtypeOf Comparable,jdk不希望再引入一个新的关键词;


其次,T既可以是类对象也可以是接口,如果是类对象应该是implements,而如果是接口,则应该是extends;从子类型上来讲,extends更接近要表达的意思。


限定符可以指定多个类型参数,分隔符是 &,不是逗号,因为在类型参数定义中,逗号已经作为多个类型参数的分隔符了,如:<T,S extends Comparable & Serializable>。


泛型限定的优点:

  • 限制某些类型的子类型可以传入,在一定程度上保证类型安全;
  • 可以使用限定类型的方法


实例如下:

public class MyGenericClass<E,T extends  Comparable & Serializable> implements MyGeneric<E> {
    @Override
    public E myMethod(E e) {
        System.out.println(e);
        return e;
    }
    public static void main(String[] args){
      // 只有MyComparable同时实现了Comparable & Serializable
      // 这里才编译通过
        MyGenericClass myGenericClass = new MyGenericClass<Integer,MyComparable>();
        myGenericClass.myMethod(1);
        myGenericClass.myMethod(1L);
    }
}
// 同时实现了两个接口
class MyComparable implements Comparable<MyComparable>,Serializable {
    @Override
    public int compareTo(MyComparable o) {
        return 0;
    }
}




【3】通配符

通配符是在泛型类使用时的一种机制,不能用在泛型定义时的泛型表达式中(这是泛型类型参数限定符)。


**1.使用类型通配符:? **


比如:List<?>,Map<?,?>。


List<?>是List<String>、List<Object>等各种泛型List的父类。


注意:List<Object> 并不是List<String>的父类!!!

2.读取List<?>的对象list中的元素时,永远是安全的。

因为不管list的真实类型是什么,它包含的都是Object。

public void myMethod(Collection<?> c) {
  for (Object e : c) {
    System.out.println(e);
  }
}


3.不能写入List<?>中的元素

因为我们不知道其元素类型,我们不能向其中添加对象。

//将任//将任意元素加入到其中不是类型安全的:
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 编译时错误

唯一的例外是null,它是所有类型的成员。


【4】有限制的通配符


<?>

允许所有泛型的引用调用


②extends

<? extends Number>     (无穷小 , Number]

只允许泛型为Number及Number子类的引用调用。

这称为子类型限定通配符,又称上边界通配符(upper bound wildcard Generics),代表继承它的所有子类型。

public void myMethod(Collection<? extends Person> coll){
  //Iterator只能用Iterator<?>或Iterator<? extends Person>
  Iterator<?> iterator  = coll.iterator();
  while(iterator.hasNext()){
    System.out.println(iterator.next());
  }   
  coll.add(Child);
  //error ?表示未知类型,虽然知道是Person的子类,但是能读不能写
}


③super

<? super Number>      [Number , 无穷大)


只允许泛型为Number及Number父类的引用调用。

超类型限定匹配符,又称下边界通配符(lower bound wildcard Generics)

public void myMethod(Collection<? super Person> coll){
  Iterator<?> iterator  = coll.iterator();
  while(iterator.hasNext()){
    System.out.println(iterator.next());
}   }


【4】泛型实现原理

泛型是通过java编译器的称为擦除(erasure)的前端处理来实现的。你可以(基本上就是)把它认为是一个从源码到源码的转换,它把泛型版本转换成非泛型版本。


泛型只在编译阶段有效,编译后类型被擦除了,也就是说jvm中没有泛型对象,只有普通对象。


擦除去掉了所有的泛型类型信息—所有在尖括号之间的类型信息都被扔掉了,比如说一个List<String>类型被转换为List。


所有对类型变量的引用被替换成类型变量的上限(通常是Object)。而且,如果返回结果代码类型不正确,会进行一个到合适类型的转换。


类型参数在运行时并不存在


这意味着它们不会添加任何的时间或者空间上的负担,这很好。不幸的是,这也意味着你不能依靠他们进行类型转换。


实例讲解如下:

① 有限定类型

public class GenericClass<T extends Comparable>{
  public T field1;
}

定义部分,即尖括号中间的部分直接擦除:

// 擦除后
public class GenericClass{}


引用部分如:

public T field1;

其中的T被替换成对应的限定类型,擦除后:

public Comparable field1;

② 没有限定类型

public class GenericClass<T>{
  public T field1;
}

那么的替换为object,即:

public class GenericClass{
  public Object field1;
}

③ 有多个限定类型

有多个限定符的,替换为第一个限定类型名。如果引用了第二个限定符的类对象,编译器会在必要的时候进行强制类型转换。

public class GenericClass<T extends Comparable & Serializable>{
  public T field1;
}

类擦除后变为:

public class GenericClass{
  public Comparable field1;
}

而表达式返回值返回时,泛型的编译器自动插入强制类型转换。


【5】泛型类被其所有实例共享


实例如下,猜测下面代码打印结果是什么:

   @Test
   public  void printCollection() {
        List<String> l1 = new ArrayList<String>();
        List<Integer> l2 = new ArrayList<Integer>();
        System.out.println(l1.getClass());
        System.out.println(l2.getClass());
        System.out.println(l1.getClass() == l2.getClass());
    }

或许你会说false,但是你想错了。它打印出true。

打印结果如下:

class java.util.ArrayList
class java.util.ArrayList
true

因为一个泛型类的所有实例在运行时具有相同的运行时类(class),而不管他们的实际类型参数。


事实上,泛型之所以叫泛型,就是因为它对所有其可能的类型参数,有同样的行为;同样的类可以被当作许多不同的类型。


作为一个结果,类的静态变量和方法也在所有的实例间共享。


这就是为什么在静态方法或静态初始化代码中或者在静态变量的声明和初始化时使用类型参数(类型参数是属于具体实例的)是不合法的原因(静态泛型方法例外)。



【6】泛型的约束和限制

① 不能使用8个基本类型实例化类型参数


原因在于类型擦除,Object不能存储基本类型:

byte,char,short,int,long,float,double,boolean


从包装类角度来看,或者说三个:

Number(byte,short,int,long,float,double),char,boolean


② 类型检查不可使用泛型

泛型类被所有其实例(instances)共享的另一个暗示是检查一个实例是不是一个特定类型的泛型类是没有意义的。


无法对参数化类型Collection<String>执行instanceof检查。因为在运行时会删除其他泛型类型信息,请使用Collection<?>代替。


③ 不能创建泛型对象数组

可以使用带范型参数值的类声明数组,却不能为数组创建泛型:

可以创建通配类型数组,然后进行强制类型转换。不过这是类型不安全的。

GenericMethod<User>[] o=
(GenericMethod<User>[]) new GenericMethod<?>[10];


不可以创建的原因是:因为类型擦除的原因无法在为元素赋值时类型检查,因此jdk强制不允许。

有一个特例是方法的可变参数,虽然本质上是数组,却可以使用泛型。

安全的方法是使用List。


④ Varargs警告


java不支持泛型类型的对象数组,可变参数是可以的。它也正是利用了强制类型转换,因此同样是类型不安全的。所以这种代码编译器会给一个警告。

public static <T> T getMiddle(T... a){
  return a[a.length/2];
}

去除警告有两种途径:一种是在定义可变参数方法上(本例中的getMiddle())加上@SafeVarargs注解,另一种是在调用该方法时添加@SuppressWarnings("unchecked")注解。


⑤ 不能实例化泛型对象


如下所示

T t= new T();//error
T.class.newInstance();//error
T.class;//error


解决办法是传入Class<T> t参数,调用t.newInstance()。

public void sayHi(Class<T> c){
  T t=null;
  try {
    t=c.newInstance();
  } catch (Exception e) {
    e.printStackTrace();
  }
  System.out.println("Hi "+t);
}


⑥ 不能在泛型类的静态域中使用泛型类型

public class Singleton<T>{
    private static T singleton; //error
    public static T getInstance(){} //error
    public static void print(T t){} //error
}


但是,静态的泛型方法可以使用泛型类型:

public static <T> T getInstance(){return null;} //ok
public static <T> void print(T t){} //ok


解释如下:


泛型类中,<T>称为类型变量,实际上就相当于在类中隐形的定义了一个不可见的成员变量:T t;,这是对象级别的,对于泛型类型变量来说是在对象初始化时才知道其具体类型的。而在静态域中,不需要对象初始化就可以调用,这是矛盾的。

静态的泛型方法,是在方法层面定义的,就是说在调用方法时,T所指的具体类型已经明确了。


⑦ 对象实例化时不指定泛型,默认为:Object


下面示例简要说明原始类型与泛型以及泛型类型:

public cl


注意,如果将原始类型赋值给参数化类型,会收到编译警告:

 Parent parent = new Parent();
 Parent<Object> objectParent = new Parent<>();
 objectParent=parent;
// Unchecked assignment: 'com.test.classes.Parent' to 
//'com.test.classes.Parent<java.lang.Object>'

参数化类型赋值给原始类型,则ok:

 Parent parent = new Parent();
 Parent<Object> objectParent = new Parent<>();
 parent =objectParent;

如果使用原始类型来调用在相应的通用类型中定义的泛型方法,则还会收到警告:

Parent parent = new Parent();
Parent<Object> objectParent = new Parent<>();
parent =objectParent;
parent.myMethod(8);
//Unchecked call to 'myMethod(T)' as a member of raw type 
//'com.test.classes.Parent'

该警告显示,原始类型绕过通用类型检查,将不安全代码的捕获推迟到运行时。因此,应避免使用原始类型。


⑧ 如果泛型类是一个接口或抽象类,则不可创建泛型类的对象。

⑨ 不能在catch中使用泛型


如下,定义一个泛型类继承自Throwable,提示如下:

public class MyThrowable<T extends Throwable > extends  Throwable{
    //Generic class may not extend 'java.lang.Throwable'
}


Throwable类不可以被继承,自然也不可能被catch。

但由于Throwable可以用在泛型类型参数中,因此可以变相的捕获泛型的Throwable对象。

public class MyThrowable<T extends Throwable >{
    public void doWork(T t) throws T {
        try {
            int i = 3 / 0;
        } catch (Throwable cause) {
            t.initCause(cause);
            throw t;
        }
    }
    public static void main(String[] args){
        MyThrowable<RuntimeException> obj=
        new MyThrowable<RuntimeException>();
        obj.doWork(new RuntimeException("why?"));
    }
}


运行结果如下:

Exception in thread "main" java.lang.RuntimeException: why?
  at com.test.classes.MyThrowable.main(MyThrowable.java:18)
Caused by: java.lang.ArithmeticException: / by zero
  at com.test.classes.MyThrowable.doWork(MyThrowable.java:9)
  ... 1 more

【7】继承泛型的参数化

一个泛型类的类型参数不同,称之为泛型的不同参数化。


泛型有一个原则:一个类或类型变量不可成为两个不同参数化的接口类型的子类型。如:

public class Parent implements Comparator {
    @Override
    public int compare(Object o1, Object o2) {
        return 0;
    }
}
class Son extends Parent  implements Comparator   {
}

这样是没有问题的。如果增加了泛型参数化:

public class Parent implements Comparator<Parent> {
    @Override
    public int compare(Parent o1, Parent o2) {
        return 0;
    }
}
class Son extends Parent  implements Comparator<Son>   {
 //'java.util.Comparator' cannot be inherited with different type 
 //arguments: 'com.test.classes.Parent' and 'com.test.classes.Son'
}

原因是Son实现了两次Comparator<T>,擦除后均为Comparator<Object>,造成了冲突。


【8】泛型与继承

① 继承泛型类时,必须对父类中的类型参数进行初始化。

或者说父类中的泛型参数必须在子类中可以确定具体类型。

例如:有一个泛型类Parent<T>,那么Son类定义时有两种方式初始化父类型的类型参数:

  • 用具体类型初始化:
public class Son extends Parent<String>{}
  • 用子类中的泛型类型初始化父类:
public class Son<T> extends Parent<T>{}

泛型类自身可以继承其他类或实现接口,如List<T>的实现ArrayList<T>

泛型类可以扩展泛型类或接口,如ArrayList<T> 实现了 List<T>,此时ArrayList<T>可以转换为List<T>。这是安全的。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, 
        java.io.Serializable{
  //...
}

G<Son>与G<Parent>

如果B是A的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G<B>并不是G<A>的子类型!

比如:String是Object的子类,但是List<String>并不是List<Object>的子类。

目录
相关文章
|
4月前
|
安全 Java
Java之泛型使用教程
Java之泛型使用教程
377 10
|
6月前
|
安全 Java API
在Java中识别泛型信息
以上步骤和示例代码展示了怎样在Java中获取泛型类、泛型方法和泛型字段的类型参数信息。这些方法利用Java的反射API来绕过类型擦除的限制并访问运行时的类型信息。这对于在运行时进行类型安全的操作是很有帮助的,比如在创建类型安全的集合或者其他复杂数据结构时处理泛型。注意,过度使用反射可能会导致代码难以理解和维护,因此应该在确有必要时才使用反射来获取泛型信息。
262 11
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
308 2
|
Java 编译器 容器
Java——包装类和泛型
包装类是Java中一种特殊类,用于将基本数据类型(如 `int`、`double`、`char` 等)封装成对象。这样做可以利用对象的特性和方法。Java 提供了八种基本数据类型的包装类:`Integer` (`int`)、`Double` (`double`)、`Byte` (`byte`)、`Short` (`short`)、`Long` (`long`)、`Float` (`float`)、`Character` (`char`) 和 `Boolean` (`boolean`)。包装类可以通过 `valueOf()` 方法或自动装箱/拆箱机制创建。
210 9
Java——包装类和泛型
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
Java 语音技术 容器
java数据结构泛型
java数据结构泛型
140 5
|
存储 安全 Java
🌱Java零基础 - 泛型详解
【10月更文挑战第7天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
174 1
|
存储 Java 编译器
Java集合定义其泛型
Java集合定义其泛型
122 1
|
存储 Java 编译器
【用Java学习数据结构系列】初识泛型
【用Java学习数据结构系列】初识泛型
123 2
|
存储 算法 Java
14 Java集合(集合框架+泛型+ArrayList类+LinkedList类+Vector类+HashSet类等)
14 Java集合(集合框架+泛型+ArrayList类+LinkedList类+Vector类+HashSet类等)
204 2
14 Java集合(集合框架+泛型+ArrayList类+LinkedList类+Vector类+HashSet类等)