Java SE -- 泛型

Java 泛型

泛型简介

泛型作为jdk1.5进入的技术,避免我们在操作集合时获取元素进行强转操作,以及其他类型元素误插入的问题。甚至他使得我们提高我们类的通用性,具体我们会在后文展开详尽介绍。

基础示例

泛型接口

接口定义,可以看到我们只需在接口上增加泛型声明即可

package com.shark.wiki.interview.javaBase.Generator;

/**
 * 泛型接口
 * @param <T>
 */
public interface GeneratorInterface<T> {
    T getVal();
}

实现类,可以看到我们的实现类同样可以不指定具体类型

/**
 * 实现泛型接口不指定类型
 * @param <T>
 */
public class GeneratorImpl<T> implements GeneratorInterface<T> {
    @Override
    public T getVal() {
        return null;
    }
}

指定类型的泛型接口继承类示例

package com.shark.wiki.interview.javaBase.Generator;

/**
 * 泛型接口指定类型
 */
public class GeneratorImpl2 implements GeneratorInterface<String> {
    @Override
    public String getVal() {
        return null;
    }

}

泛型方法

声明泛型方法的方式很简单,只需在返回类型前面增加一个 即可

package com.shark.wiki.interview.javaBase.Generator;

import org.omg.PortableServer.LIFESPAN_POLICY_ID;

import java.util.ArrayList;
import java.util.List;

/**
 * 泛型方法
 */
public class GeneratorMethod {
    public static <E> void printArray(List<E> array){
        for (E e : array) {
            System.out.println(e);
        }
    }

    public static void main(String[] args) {
        List<String> list=new ArrayList<>();
        list.add("11");
        list.add("11");
        list.add("11");
        list.add("11");
        list.add("11");
        list.add("11");
        GeneratorMethod.printArray(list);
    }
}

泛型类

与泛型接口用法差不多,在类名后面增加即可。

/**
 * 泛型类的用法
 * @param <T>
 */
public class GenericObj<T> {
    private T key;

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }


    public static void main(String[] args) {
        GenericObj<Integer> obj=new GenericObj();
        obj.setKey(1);

    }
}

泛型的使用场景

泛型大部分是应用于项目开发中通用对象例如我们常用的Map

img

什么是泛型擦除,为什么要泛型擦除呢

证明1——反射存入非泛型元素

Java本质就一门伪泛型语言,泛型的作用仅仅在编译期间进行类型检查的,一旦生成字节码之后,关于泛型的一切都会消失。

如下所示,Integer类型数组我们完全可以通过反射将字符串存到列表中。

public static void main(String[] args) throws Exception {
        List<Integer> list=new ArrayList<>();
        list.add(1);
//        list.add("s"); 报错
        Class<? extends List> clazz=list.getClass();
//        java的泛型时伪泛型,运行时就会被擦除
        Method add = clazz.getDeclaredMethod("add", Object.class);
        add.invoke(list,"k1");
        System.out.println(list);
        /**
         * 输出结果
         * [1, k1]
         */

    }

证明2——泛型形参重载失败

设计者将Java泛型在编译器后擦除的原因还有如下原因: 1. 避免引入泛型创建没必要的新类型 2. 节约虚拟机开销

这一点我们用如下的例子就能看出,相同参数不通泛型的方法根本不能重载

img

既然编译器要把泛型擦除,为什么还要用泛型呢?用Object不行嘛?

  1. 使用泛型后便于集合的取操作,且提高的代码的可读性
  2. 如下代码所示,虽然一下代码在编译后会擦除为Object类型,但是通过泛型限定后,jvm就会自动将其强转为Comparable类型,减少我们编写一些没必要的代码
public class Test2 {
    public static void main(String[] args) {
        List<? extends Comparable> list=new ArrayList<>();
        for (Comparable comparable : list) {
            comparable.compareTo("1");
        }
    }
}

img

什么是桥方法

示例

编写一个带有泛型的父类

class Pair<T> {

    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

子类继承该类,设置泛型为Date,并重写该方法

public class DateInter extends Pair<Date> {
    @Override
    public void setValue(Date value) {
        super.setValue(value);
    }

    @Override
    public Date getValue() {
        return super.getValue();
    }


}

测试代码

public static void main(String[] args) {
        DateInter d = new DateInter();
        d.setValue(new Date());
//        d.setValue(new Object());//编译报错,所以setValue被重写了
    }

重写的真相,桥方法

我们都知道泛型会在编译时期被擦除,所以我们父类的getValue应该是返回Object类型,而子类所谓"重写"用的却是Date类型。这根本不成立。一个Object类型的setValue,一个Date类型的setValue,这种情况若是我们自己的写的代码编译器都过不了。

所以这一切都在告诉我们泛型方法的重写根本就是幌子,我们通过下面这段命令反编译一下字节码

javap -c DateInter

可以看到下面这段代码,实际上是生成一个Object类型的同方法名的桥方法(如果我们方法名相同返回值不同是不会通过编译的,但是桥方法可以),"重写"的方法调用这些桥方法,使得我们的代码从便面上看重写成功了,这就是所谓的桥方法。

public class com.zsy.onJava8.generic.brige.DateInter extends com.zsy.onJava8.generic.brige.Pair<java.util.Date> {
  public com.zsy.onJava8.generic.brige.DateInter();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method com/zsy/onJava8/generic/brige/Pair."<init>":()V
       4: return

  public void setValue(java.util.Date);
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #2                  // Method com/zsy/onJava8/generic/brige/Pair.setValue:(Ljava/lang/Object;)V
       5: return

  public java.util.Date getValue();
    Code:
       0: aload_0
       1: invokespecial #3                  // Method com/zsy/onJava8/generic/brige/Pair.getValue:()Ljava/lang/Object;
       4: checkcast     #4                  // class java/util/Date
       7: areturn


//下面这两个方法都是jvm为我们生成的桥方法,jvm允许桥方法方法名和"重写"的方法名一样,通过桥方法的调用,完成表象上的重载
  public void setValue(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #4                  // class java/util/Date
       5: invokevirtual #5                  // Method setValue:(Ljava/util/Date;)V
       8: return

  public java.lang.Object getValue();
    Code:
       0: aload_0
       1: invokevirtual #6                  // Method getValue:()Ljava/util/Date;
       4: areturn
}

泛型有哪些限制?

泛型不可以被实例化,如下所示

泛型会在编译器擦除,所以泛型在编译器还未知,所以不可被实例化

img

泛型参数不可以是基本类型

我们都知道泛型仅在编译器存在,当编译结束泛型就会被擦除,对象就会编程Object类型,所以基本类型作为泛型参数ide就会直接报错

img

泛型无法被实例化,无论是泛型变量还是泛型数组

从上文我们就知道泛型会在编译期完成后被擦除,这正是因为jvm不想为泛型创建新的类型造成没必要的开销

不能抛出或者捕获T类型的泛型异常

之所以catch使用泛型会编译失败,是因为若引入泛型后,编译器无法直到这个错误是否是后续catch类的父类

img

不能声明泛型错误

如下图所示,泛型会在编译器被擦除,那么下面这段代码的catch就等于catch两个一样的错误,出现执行矛盾。

try{
}catch(Problem<String> p){
}catch(Problem<Object> p){
}

img

不能声明两个参数一样泛型不同的方法

编译器擦除后,参数一样,所以编译失败

泛型不能被声明为static

泛型只有在类创建时才知晓,而静态变量在类加载无法知晓,故无法通过编译

img

以下代码是否能编译,为什么?

例1

    public final class Algorithm {
        public static <T> T max(T x, T y) {
            return x > y ? x : y;
        }
    }
答:错误,T类型未知,无法比较,编译失败

例2

    public class Singleton<T> {

        public static T getInstance() {
            if (instance == null)
                instance = new Singleton<T>();

            return instance;
        }

        private static T instance = null;
    }

答案

不能,泛型不能被static修饰

泛型的通配符

什么是通配符,它用于解决什么问题

我们都知道通配符是解决泛型之间无法协变的问题,当我们使用一种类型作为泛型参数时,却无法使用他的父类或者子类进行赋值,而通配符就是解决这种问题的对策。

上界通配符

简介

有时我们不知道子类的具体类型,上界通配符就是用于解决那些父类引用指向子类泛型引用的场景,所以上界通配符的设计增强了代码的通用性。

上界通配符使用示例

定义父类

/**
 * 水果父类
 */
public class Fruit {
}

子类代码

/**
 * 水果的子类 苹果
 */
public class Apple extends Fruit {
}

容器类代码

/**
 * 容器类
 * @param <T>
 */
public class Container<T> {
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

测试代码,如下所示上界通配符使得苹果类可以作为水果类的指向引用。

/**
 * 泛型测试
 */
public class TestParttern {
    public static void main(String[] args) {
        Container<? extends Fruit> container=new Container<Apple>();
        Fruit data = container.getData();
        container.setData(new Apple());
    }
}

为什么上界通配符只能get不能set

如上代码所示,当我们用上界通配符? extends Fruit,我们用其子类作为泛型参数,这只能保证我们get到的都是这个子类的对象。

但我们却忘了一点,当我们用子类apple作为泛型参数时,泛型的工作机制仅仅是对这个对象加个一个编号CAP#1,当我set一个新的对象,编译器无法识别这个对象类型是否和编号匹配。

更通俗的理解,上界通配符决定可以指向的容器,但是真正使用是并不知晓这个容器是哪个子类容器。所以无法set。

img

下界通配符

下界通配符使用示例

这里使用的对象还是上述对象,只不过通配符改为下界通配符

/**
 * 泛型测试
 */
public class TestParttern {

    public static void main(String[] args) {

        Container<? super Apple> container1=new Container<Fruit>();
    }
}

下界通配符原理介绍

下界通配符决定了泛型的最大粒度的上限,通俗来说只要是苹果类的父亲都可以作为被指向的引用。通过super声明,它可以很直观的告诉我们泛型参数必须传super后的父类如下所示

 Container<? super Apple> container1=new Container<Fruit>();

为什么下界通配符只能set不能get(或者说get的是object)

原因如下: 1. 下界通配符决定泛型的类型上限,所有水果类的父亲都可以作为指向的引用 2. get时无法知晓其具体为哪个父亲,所以取出来的类型只能是object

 Container<? super Apple> container1=new Container<Fruit>();
        Object data = container1.getData();

泛型作为元组返回多对象

场景简介

有时某个函数需要返回多个对象,而return只能返回一个对象,我们当然可以通过创建类的方式解决问题,但如果需要考虑多返回值类型众多,为了通用性我们也建议使用泛型元组解决问题

代码示例

返回两个对象的元组类,可以看到我们的返回对象都用public,有人可能会说这不是就导致安全性问题,实际上仔细查看代码我们使用final关键字解决了这个问题

public class Tuple2<A, B> {
    public final A a1;
    public final B a2;
    public Tuple2(A a, B b) { a1 = a; a2 = b; }
    public String rep() { return a1 + ", " + a2; }

    @Override
    public String toString() {
        return "(" + rep() + ")";
    }
}

三个返回值类型的元组类

/**
 * 基于父类的元组扩展
 *
 * @param <A>
 * @param <B>
 * @param <C>
 */
public class Tuple3<A, B, C> extends Tuple2<A, B> {
    pulic final C obj3;

    public Tuple3(A obj1, B obj2, C obj3) {
        super(obj1, obj2);
        this.obj3 = obj3;
    }

    @Override
    public void rep() {
        System.out.println("obj1:" + obj1 + "   obj2:" + obj2 + "   obj3:" + obj3);
    }



}

使用示例

public static void main(String[] args) {
        Tuple2<Integer, String> tuple2 = new Tuple2<Integer, String>(1, "hello");
        tuple2.rep();
        Tuple3<Integer,String, Automobile> tuple3=new Tuple3<>(1,"hello",new Automobile());
        tuple3.rep();
    }

使用泛型其他示例

实现一个链式栈类

// generics/LinkedStack.java
// 用链式结构实现的堆栈

public class LinkedStack<T> {
    private static class Node<U> {
        U item;
        Node<U> next;

        Node() { item = null; next = null; }

        Node(U item, Node<U> next) {
            this.item = item;
            this.next = next;
        }

        boolean end() {
            return item == null && next == null;
        }
    }

    private Node<T> top = new Node<>();  // 栈顶

    public void push(T item) {
        top = new Node<>(item, top);
    }

    public T pop() {
        T result = top.item;
        if (!top.end()) {
            top = top.next;
        }
        return result;
    }

    public static void main(String[] args) {
        LinkedStack<String> lss = new LinkedStack<>();
        for (String s : "Phasers on stun!".split(" ")) {
            lss.push(s);
        }
        String s;
        while ((s = lss.pop()) != null) {
            System.out.println(s);
        }
    }
}

继承列表实现泛型随机存取列表类

// generics/RandomList.java
import java.util.*;
import java.util.stream.*;

public class RandomList<T> extends ArrayList<T> {
    private Random rand = new Random(47);

    public T select() {
        return get(rand.nextInt(size()));
    }

    public static void main(String[] args) {
        RandomList<String> rs = new RandomList<>();
        Arrays.stream("The quick brown fox jumped over the lazy brown dog".split(" ")).forEach(rs::add);
        IntStream.range(0, 11).forEach(i -> 
            System.out.print(rs.select() + " "));
    }
}

如何获取泛型类型

public class GenericType<T> {
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public static void main(String[] args) {
    //注意这个类要使用子类,笔者为了方便期间使用了 {}
        GenericType<String> genericType = new GenericType<String>() {};
        Type superclass = genericType.getClass().getGenericSuperclass();
        //getActualTypeArguments 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]
        Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; 
        System.out.println(type);//class java.lang.String
    }
}

泛型数组相关面试题

泛型数组:能不能采用具体的泛型类型进行初始化?

不能,如下代码所示第一行就报错了,原因很简单,由于泛型擦除机制 导致数组lsa[1]存放的是List元素为object类型,要想向下面代码这样拿到String类必须要强转才行,这就违背了泛型的原则,所以Java不支持泛型数组。

        List<String>[] lsa = new List<String>[10]; // Not really allowed.
//        转obj再转obj数组
        Object o = lsa;
        Object[] oa = (Object[]) o;
//        声明一个list添加元素
        List<Integer> li = new ArrayList<Integer>();
        li.add(new Integer(3));
//        存到转来的obj数组中
        oa[1] = li; // Unsound, but passes run time store check
/**
 * 由于泛型擦除机制 导致数组lsa[1]存放的是List元素为object类型,要想向下面代码这样拿到String类必须要强转才行
 */
        String s = lsa[1].get(0); // Run-time error ClassCastException.

所以java仅仅支持通配符数组

//通配符需要做显示强转,所以编译通过
        List<?>[] lsa = new List<?>[10];
//        转obj再转obj数组
        Object o = lsa;
        Object[] oa = (Object[]) o;
//        声明一个list添加元素
        List<Integer> li = new ArrayList<Integer>();
        li.add(new Integer(3));
//        存到转来的obj数组中
        oa[1] = li; // Unsound, but passes run time store check

        Integer integer = (Integer) lsa[1].get(0); // Run-time error ClassCastException.
        System.out.println(integer);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值