泛型

1,412 阅读11分钟

什么是泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性,

泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。

泛型的本质是为了参数化类型,或者说可以将类型当作参数传递给一个类或者是方法。

类型参数(又称类型变量)用作占位符,在运行时为类分配类型。

泛型类型可以是多个不同的类型,但编译后但都是相同但类型(泛型擦除)

向上转型:子类对象转为父类,父类可以是接口。公式:Father f = new Son();Father是父类或接口,son是子类。

向下转型:父类对象转为子类。公式:Son s = (Son)f;

编译器工作方式

由于泛型类型的擦除,以致JVM无法访问到泛型的类型信息。

所以Java会在编译器会先检查代码中泛型的类型,不符合泛型规范会报错,符合泛型规范则进行擦除,再进行编译。

泛型的作用

类型检查

List<String> l = new ArrayList<>();
l.add("abc");
l.add(1);//会报错。类型不一致

类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。

检测原则:变量类型中的泛型,和实例类型中的泛型,必须保证相同(不支持继承关系)

也就是l变量,指定l集合只能接受String类型的元素,如果add的元素不是String类型就会报错

//同样真正涉及类型检查的是它的引用,我们使用它的引用Str调用方法,所以Str引用能完成范型的检查
ArrayList<String> str = new ArrayList();

//ArrayList string 是jdk5以前的集合表达,没有泛型,也就不存在类型检查
//这种写法是兼容jdk5以前的集合,但是不存在类型检查
ArrayList string = new Arraylist<String>();//这样就没有效果

自动类型转换

作用:避免显性强转

如果没有泛型。那么就只能使用显性强转。如下代码所示

List l = new ArrayList();
l.add("abc");
//l.get(0);默认拿到的是object类型
String s = (String) l.get(0);

而使用泛型,就可以保证存入和取出的都是String类型 不必在进行cast了。比如:

List<String> l = new ArrayList<>();
l.add("abc");
String s = l.get(0);

既然范型变量最后都被替换成了原始类型,那么,为什么我们获取的时候,不需要进行强制类型转换?

java
public E get(int index) {  
    RangeCheck(index);  
    return (E) elementData[index];  
    } 

可以看出,在return之前,会根据泛型变量进行强转。

所以我们无需再显性转换

带来的好处:

  • 代码更加简洁【不用强制转换】
  • 可读性和稳定性【在编写集合的时候,就限定了类型】
  • 类型检查,程序更加健壮。(泛型提供了一种类型检测的机制,将运行期的错误转换到编译期,只有相匹配的数据才能正常的赋值 如果我们在对一个对象所赋的值不符合其泛型的规定, 就会编译报错)
  • 避免强转,类型安全(早期Java是使用Object来代表任意类型的,但是向下转型有强转的问题,强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。

泛型规范

根据惯例,类型参数是单个大写字母,该字母用于指示所定义的参数类型。下面列出每个用例的标准类型参数:

  • E:元素
  • K:键
  • N:数字
  • T:类型
  • V:值
  • S、U、V 等:多参数情况中的第 2、3、4 个类型

泛型的基本使用

泛型类

/**
 * 子类不明确泛型类的类型参数变量:
 *      实现类也要定义出<T>类型的
 *
 */
public class InterImpl<T> implements Inter<T> {

    @Override
    public void show(T t) {
        System.out.println(t);

    }
}

子类不明确泛型类的类型参数变量

当子类不明确泛型类的类型参数变量时,外界使用子类的时候,也需要传递类型参数变量进来,在实现类上需要定义出类型参数变量

/**
 * 子类不明确泛型类的类型参数变量:
 *      实现类也要定义出<T>类型的
 *
 */
public class InterImpl<T> implements Inter<T> {

    @Override
    public void show(T t) {
        System.out.println(t);

    }
}

测试代码:

public static void main(String[] args) {
        //测试第一种情况
        //Inter<String> i = new InterImpl();
        //i.show("hello");

        //第二种情况测试
        Inter<String> ii = new InterImpl<>();
        ii.show("100");

    }

泛型接口

/*
    把泛型定义在接口上
 */
public interface Inter<T> {
    public abstract void show(T t);

}

实现泛型接口的类.....

/**
 * 子类明确泛型类的类型参数变量:
 */

public class InterImpl implements Inter<String> {
    @Override
    public void show(String s) {
        System.out.println(s);

    }
}

实际上不同重写,而是重载

InterImpl是show(String s) ,Inter依然还是show(Object s)

Inter的show(Object s)会调用重写的InterImpl的show(String s)方法。

泛型方法

外界仅仅是关心该方法,不关心类其他的属性...这样的话,我们在整个类上定义泛型,未免就有些大题小作了。

    //定义泛型方法..
    public <T> void show(T t) {
        System.out.println(t);

    }

只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。

如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。

泛型方法与可变参数

再看一个泛型方法和可变参数的例子:

public <T> void printMsg( T... args){
    for(T t : args){
        Log.d("泛型测试","t is " + t);
    }
}

泛型和static

对于一个static的方法,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。

泛型嵌套

学生类:

package generic;

public class Stu<T> {

    private T score;

    public T getScore() {
        return score;
    }

    public void setScore(T score) {
        this.score = score;
    }
    
}

学校类:

package generic;

/**
 * @author Administrator
 *
 * @param <T>
 */
public class School<T> {
    private T stu;

    public T getStu() {
        return stu;
    }

    public void setStu(T stu) {
        this.stu = stu;
    }
    
}

测试

        //泛型嵌套:
        School<Stu<String>> sch = new School<Stu<String>>();
        sch.setStu(stu);
        stu = sch.getStu();
        String score = stu.getScore();
        System.out.println(score);

多种泛型类型

通过在尖括号之间放置一个逗号分隔的类型列表,可在类或接口中使用多个类型参数。

public class MultiGenericContainer<T, S> {
    private T firstPosition;
    private S secondPosition;
   
    public MultiGenericContainer(T firstPosition, S secondPosition){
        this.firstPosition = firstPosition;
        this.secondPosition = secondPosition;
    }
    
    public T getFirstPosition(){
        return firstPosition;
    }
    
    public void setFirstPosition(T firstPosition){
        this.firstPosition = firstPosition;
    }
    
    public S getSecondPosition(){
        return secondPosition;
    }
    
    public void setSecondPosition(S secondPosition){
        this.secondPosition = secondPosition;
    }
    
}

通配符

?号通配符表示可以匹配任意类型,任意的Java类都可以匹配.....

?和T的区别

T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,

?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。

JAVA泛型通配符T,E,K,V区别,T以及Class,Class的区别

通配符边界

无边界通配符

无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。

采用<?>的形式

public void test(List<?> list){


    for(int i=0;i<list.size();i++){
        
        System.out.println(list.get(i));
    
    }
}

注意:就只能调对象与类型无关的方法,不能调用对象与类型有关的方法。如:List<?> 的add是不能调用的

通配符上限

使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据。 要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界. 注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类.

泛型擦除后用原始类型E代替

例如:

public static void addTest2(List<? extends Number> l) {
// l.add(1); // 编译报错
// l.add(1.1); // 编译报错 
   l.add(null);
   Number number = l.get(1); // 正常 
}

List集合装载的元素只能是Number的子类或自身

泛型擦除后用Number代替

注意:

  • 不能使用add方法(除了add(null))(我们不确定该List的类型,也就不知道add方法的参数类型。可能是long也可能是Double)

  • 使用get方法只能拿到Number类型的数据,(因为知道数据一定是Number的子类。但是不知道是什么子类。)

通配符下限

使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据。 要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界.

泛型擦除后用原始类型Object代替

例如:

    //传递进来的只能是Integer或Integer的父类
public static void getTest2(List<? super Integer> list) {
 // Integer i = list.get(0); //编译报错 
    Object o = list.get(1);
}

注意:

慎用get方法

因为List<? super E>使用get方法只能拿到Object类型数据

原因是,泛型擦除后用Object代替

PECS原则

  • 频繁往外读取内容,适合用上界Extends
  • 经常往里插入的,适合用下界Super

通配符和泛型方法怎么选择?

如果参数之间的类型有依赖关系,或者返回值是与参数之间有依赖关系的。那么就使用泛型方法

如果没有依赖关系的,就使用通配符,通配符会灵活一些,并且通配符对于集合没有add/get等操作数据的限制

泛型原理(泛型擦除)

原理:泛型擦除

泛型信息只存在于代码编译阶段,在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

原始类型

原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。

  • 非通配符上限,编译时会将类型变量擦除,使用Object代替
  • 通配符上限,编译时会将类型变量擦除,使用其限定类型代替(如 ? extends Number,会用Number代替)

List的类型擦除

  • List、List擦除后的类型为List。
  • List[]、List[] 擦除后的类型为 List[]。
  • List<? extends E>、List<? super E>擦除后的类型为List。
  • List<T extends Serialzable & Cloneable>擦除后类型为 List。

以下情况不能用泛型类型

因为被擦除时都会转成object获取其他限定类型

1、泛型类型变量不能是基本数据类型

因为泛型擦除后是Object或者限定类型,Object类型不能存储double值,只能引用Double的值。

自动装箱和拆箱操作能够在使用泛型对象时将值存储为原始类型并检索原始类型的值。

2、不能抛出也不能捕获泛型类的对象

根据异常捕获的原则,一定是子类在前面,父类在后面。在编译之后还是会变成Throwable,ArrayIndexOutofBounds是IndexOutofBounds的子类,违背了异常捕获的原则。所以java为了避免这样的情况,禁止在catch子句中使用泛型变量。

3、不能声明参数化类型的数组。

4、泛型类型的实例化。

5、泛型在静态方法和静态类中的问题

泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数

静态方法或者变量不依赖类

扩展

泛型能被继承吗?

泛型不具备继承性。

例如

List<Integer> list4 = new ArrayList<>();
List<Number> list5 = list4;//会报错

但是jdk保留了集合类型参数的上限。

当把带有泛型特性的集合赋值给老版本的集合时候,会把泛型给擦除了。

 List<String> list = new ArrayList<>();
//类型被擦除了,保留的是类型的上限,String的上限就是Object
List list1 = list;


List list = new ArrayList();
//它也不会报错,仅仅是提示“未经检查的转换”
List<String> list2 = list;

T,Class,Class<?>区别

Class在实例化的时候,T要替换成具体类

Class<?>它是个通配泛型,?可以代表任何类型,主要用于声明时的限制情况

为什么说泛型是假泛型?

真泛型是什么?不论是编译期还是运行期泛型都存在,比如C#泛型

而java泛型只在编译期,运行期不存在。

例如List和List在java运行期都是List

可以利用反射跳过泛型类型检测

类型擦除与多态的冲突和解决方法

/**
 * 子类明确泛型类的类型参数变量:
 */

public class InterImpl implements Inter<String> {
    @Override
    public void show(String s) {
        System.out.println(s);

    }
}

实际上不同重写,而是重载

InterImpl是show(String s) ,Inter依然还是show(Object s)

Inter的show(Object s)会调用重写的InterImpl的show(String s)方法。

绕过正常开发中编译器不允许的操作限制。

Java泛型原理及常用方式 Java泛型详解 Java泛型详解 泛型就这么简单

java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一