美文网首页
Java泛型

Java泛型

作者: Android_Gleam | 来源:发表于2020-09-25 14:47 被阅读0次

1、为什么需要泛型?

1.1可以为多种数据类型执行相同的代码

public class NonGeneric {
    
    public int addInt(int x,int y){
        return x + y;
    }

    public float addFloat(float x,float y){
        return x + y;
    }

    public static void main(String[] args) {
        NonGeneric nonGeneric = new NonGeneric();
        System.out.println(nonGeneric.addInt(1,2));
        System.out.println(nonGeneric.addFloat(1f,2f));
    }
    
}

我们可以看到上面这段代码,int类型,float类型都有各自的加法方法,如果我们还要增加double类型的话,我们还要再加一个double的加法方法,这样显然是麻烦的。

1.2可以在编译期间指定数据类型,不需要强制类型转换,以及插入错误的数据类型在编译期间就能发现。

public class NonGeneric {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("String");
        list.add(100);
        int size = list.size();
        for (int i = 0; i < size; i++) {
            String name = (String) list.get(i);
            System.out.println("name:" + name);
        }
    }
}
运行后报错 image.png

2、泛型类、泛型接口和泛型方法

2.1泛型方法

完全独立的,不一定非要定义在泛型类或泛型接口中,在普通类中同样可以定义。
在访问修饰符和返回值之间声明<T>,这种方法就是泛型方法,这是必要的,否则不是泛型方法。

public static <T> T genericMethod(T... a){
        return a[0];
    }

2.2泛型类和泛型接口的定义

//泛型类
public class NormalGeneric<T> {
    private T data; 
    
    //该方法不是泛型方法 只不过返回值是泛型T 只有加入<T>的方法才是泛型方法
    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
//泛型接口
public interface IGenertor<T> {
    T next();
}
//泛型接口实现类1 不指明确切类型
public class ImpGenertor<T> implements IGenertor<T> {
    @Override
    public T next() {
        return null;
    }
}
//泛型接口实现类2 明确具体类型
public class ImpGenertor2 implements IGenertor<String> {
    @Override
    public String next() {
        return null;
    }
}

泛型方法和泛型类或泛型接口的区别
泛型类或泛型接口在创建实例对象时传入具体类型,泛型方法在调用时传入(不传入也可以,编译器会自动识别)

public class NonGeneric {

    public static <T> T genericMethod(T... a){
        return a[0];
    }

    public static void main(String[] args) {
        //泛型类
        ImpGenertor<String> impGenertor = new ImpGenertor();
        //泛型接口 自动识别类型
        System.out.println(genericMethod("111","2222"));
    }
}

泛型方法和泛型类或泛型接口 可以有多个泛型
例如:

public interface IGenertor<T,K> {
    T next();
    K firrs();
}

下面我们看一段代码

public class NonGeneric {
    static class Fruit{
        @Override
        public String toString() {
            return "水果";
        }
    }

    static class Apple extends Fruit{
        @Override
        public String toString() {
            return "苹果";
        }
    }

    static class Person{
        @Override
        public String toString() {
            return "人";
        }
    }

    static class GenericTest<T> {

        public void show_1(T t) {
            System.out.println(t.toString());
        }
        
        public <E> void show_2(E e) {
            System.out.println(e.toString());
        }

        public <T> void show_3(T t) {
            System.out.println(t.toString());
        }

    }

    public static void main(String[] args) {
        Fruit fruit = new Fruit();
        Apple apple = new Apple();
        Person person = new Person();

        GenericTest<Fruit> genericTest = new GenericTest<>();

        genericTest.show_1(fruit);
        genericTest.show_1(apple);
        //编译器会报错
//        genericTest.show_1(person);

        genericTest.show_2(apple);
        genericTest.show_2(fruit);
        genericTest.show_2(person);

        genericTest.show_3(apple);
        genericTest.show_3(fruit);
        genericTest.show_3(person);
    }
}

由上面的代码可知:
1、不是声明在泛型类里面的就是泛型方法
2、在泛型类中使用泛型方法,也要在访问修饰符和返回值之间将泛型<T>声明出来
3、泛型方法声明的泛型是独立的,所以show_2可以传入任意类型,不和泛型类中定义的泛型有任何关系
4、如果泛型方法声明的泛型和泛型类中声明的泛型名称一致,也遵循上一条,所以show_3使用上与show_2没有任何区别
5、泛型类声明的泛型只影响泛型类中使用该泛型的部分,不会影响独立的泛型方法。

3、如何限定类型变量

public static <T> T min(T a,T b){
        if (a.compareTo(b) > 0) return a; else return b;
    }

上面这段代码编译器会报错,因为编译器不能确定T类型一定含有compareTo方法,那么我们如何保证执行泛型这个类型变量一定有某个方法或遵循某种规则呢?
我们如果在传入参数时,限制只有实现compareTo方法才可以传入是不是就解决了这个问题,这个思想就是类型变量的限定。
我们将代码改为下面这种形式

public static <T extends Comparable> T min(T a,T b){
        if (a.compareTo(b) > 0) return a; else return b;
    }

这样就没问题了,限制T必须实现或者派生自Comparable这个接口
T表示应该绑定类型的子类型
Comparable表示绑定类型也就是限定类型
T和Comparable 可以是类 也可以是接口
限定类型可以是多个,用&连接,但是要注意,如果限定类型有接口,也有类,类必须要写在限定类型的第一个,而且必须是有且仅有一个类。

//这么写会报错
public static <T extends Comparable& ArrayList> T min(T a, T b){
        if (a.compareTo(b) > 0) return a; else return b;
    }
//这么写没问题
  public static <T extends ArrayList&Comparable> T min(T a, T b){
        if (a.compareTo(b) > 0) return a; else return b;
    }

为什么只能有一个类?
因为Java是单继承的
为什么一定要写在第一个?
问Java去吧 我也不知道
注意:上面写的示例是泛型方法,但是规则对泛型类和泛型接口同样适用。

4、泛型使用中的约束和局限性

1、不能实例化类型变量
2、静态域或者静态方法里不能使用,需要注意的是静态方法本身是泛型方法就可以使用
3、基本数据类型不可以,可以传入基础类型的包装类,为什么不可以,因为T只能是对象,基础数据类型不是对象
4、不能使用instanceof关键字 判断具体类型
5、泛型在使用的时候 关于这个类的类型不管你传入的是什么类型参数 打印出来的类名都是一样的
6、可以定义泛型数组 但是不能初始化
7、泛型类不能extends Exception/Throwble
8、不能捕获泛型类对象

public class Restrict<T> {
    private T data;

    //1、不能实例化类型变量
//    public Restrict() {
//        this.data = new T();
//    }

    //2、静态域或者静态方法里不能使用
//    private static T instance;
    //静态方法本身是泛型方法就可以使用
//    private static <T> T getInstance(){
//        T a = null;
//        return a;
//    };

    public static void main(String[] args) {
        //3、基本数据类型不可以会报错
//        Restrict<double>

        Restrict<Double> restrict = new Restrict<>();
        //4、不能使用instanceof关键字 判断具体类型 这里会报错
//        if(restrict instanceof Restrict<Double>)

        //5、泛型在使用的时候 关于这个类的类型不管你传入的是什么类型参数 打印出来的类名都是一样的
        Restrict<String> restrictStr = new Restrict<>();
        //这里打印结果为true
        System.out.println(restrict.getClass() == restrictStr.getClass());
        //下面打印的名字是一样的  Restrict类名  泛型在使用的时候 关于这个类的类型不管你传入的是什么类型参数 打印出来的类名都是一样的
        System.out.println(restrict.getClass().getName());
        System.out.println(restrictStr.getClass().getName());

        //打印结果
//        true
//        com.hot.lib.day1.Restrict
//        com.hot.lib.day1.Restrict

        //6可以定义泛型数组 但是不能初始化
        Restrict<Double>[] restrictArray;
        Restrict<Double>[] restricts = new Restrict<Double>[10];

    }

    //7、泛型类不能extends Exception/Throwble 这里编译器会报错
//    private class Problem<T> extends Exception{}

    //8、不能捕获泛型类对象
//    public <T extends Throwable> void dowork(T x){
//        try {
//
//        }catch (T t){
//
//        }
//    }
    
    //这种方式可以
    public <T extends Throwable> void dowork(T x) throws T{
        try {

        }catch (Throwable t){
            throw x;
        }
    }
}

5、泛型类型的继承规则

这里我们用代码来提现

class Employee {
}

class Worker extends Employee {
}

public class Pair<T> {

    public static void main(String[] args) {
        Pair<Worker> workerPair = new Pair<>();
        Pair<Employee> employeePair = new Pair<>();

        //我们平时这么写是没问题的  因为Worker extends Employee
        Employee employee = new Worker();

        //1、这里编译器会报错  workerPair和employeePair 不存在继承关系
//        Pair<Employee> employeePair1 = new Pair<Worker>();

        //这里是没问题的
          Pair<Employee> pair = new ExtendPair<>();
        
    }

    //2、泛型类可以继承或者扩展其他泛型类 比如List和ArrayList
    private static class ExtendPair<T> extends Pair<T>{}
}

6、泛型中的通配符类型

只能用在方法中,类中是不可以使用的
这里比较复杂,首先我们先看几个测试类。

public class GenericType<T> {
    private T data;

    public T getData() {
        return data;
    }

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

public class Food {
}

public class Fruit extends Food{
}

public class Apple extends Fruit{
}

public class Orange extends Fruit{
}

public class Tomatoes extends Apple {
}

注意上面这些类的继承关系。

我们首先看第一种情况

 public static void println(GenericType<Fruit> p){
        System.out.println(p.getData());
    }

    public static void use(){
        GenericType<Fruit> fruitGenericType = new GenericType<>();
        GenericType<Apple> appleGenericType = new GenericType<>();
        println(fruitGenericType);
        //编译器报错
//        println(appleGenericType);
    }

这种情况很简单,我们上面说过,泛型参数之间存在继承关系,但是泛型类没有任何关系,所以这里传入GenericType<Apple>类型会报错。正是因为如此,才引入了通配符的概念。

接下来我们看下面这段代码

public static void println(GenericType<? extends Fruit> p){
        System.out.println(p.getData());
    }

    public static void use(){
        GenericType<Fruit> fruitGenericType = new GenericType<>();
        GenericType<Apple> appleGenericType = new GenericType<>();
        GenericType<Food> foodGenericType = new GenericType<>();
        println(fruitGenericType);
        //这时可以编译通过
        println(appleGenericType);
        //编译报错
//        println(foodGenericType);

        GenericType<? extends Fruit> genericType = new GenericType<>();
        //不会报错
        Fruit fruit = genericType.getData();
        //下面两行代码都会报错
//        genericType.setData(new Apple());
//        genericType.setData(new Fruit());
    }

这里我们可以看到打印方法的泛型改为? extends Fruit,?就是通配符,extends则是我们上面讲的限定符(上界限定符),这里的意思是,限定泛型类型为Fruit的派生类,包括Fruit本身。所以我们看到,println传入Fruit的子类型都没问题了,但是传入Food就会报错,因为Food是Fruit的父类。

接下来我们看到,我们创建了一个genericType对象,然后分别调用了getData和setData方法。
getData方法返回的是Fruit类型,因为我们知道传入的上限就是Fruit类型,如果传入的是Fruit派生类型,也是可以转为Fruit类型,所以这里会返回Fruit类型。

然后我们发现,setData方法会报错,因为我们没有指定确切的类型,虽然我们传入的是Fruit的子类,但是编译器不知道是哪个子类,所以会报错。

extends限定符的作用就是可以安全的访问数据,也就是extends后面的类型数据,即例子中的Fruit类型数据。

接下来我们看另外一种情况

public static void println(GenericType<? super Apple> p){
        System.out.println(p.getData());
    }

    public static void use(){
        GenericType<Fruit> fruitGenericType = new GenericType<>();
        GenericType<Apple> appleGenericType = new GenericType<>();
        GenericType<Orange> orangeGenericType = new GenericType<>();
        GenericType<Tomatoes> tomatoesGenericType = new GenericType<>();
        println(fruitGenericType);
        println(appleGenericType);
        //下面两行编译报错
//        println(orangeGenericType);
//        println(Tomatoes);

        GenericType<? super Apple> genericType = new GenericType<>();
        //不会报错
        Object fruit = genericType.getData();
        //不会报错
        genericType.setData(new Apple());
        //编译报错
//        genericType.setData(new Fruit());
    }

这种情况与上面的区别就是将extends换成了super,super是下界限定符,即只能是Apple的父类及其自身。所以我们看到只能传入父类型,传入子类型或平级的类型会报错。

这时调用getData方法,会返回Object类型,不会返回具体类型,这很好理解,因为我们限制了只能传入Apple的父类型,但是我们并不知道具体是哪个父类型,因为Object是所有类的基类,所以这里返回了Object。

然后我们看setData调用,这里的逻辑可能会有些让人不舒服,我们可以看到传入Apple的父类型,也就是Fruit类型会报错,大家一定会疑惑这是为什么,我们明明限定的是只能是Apple的父类型啊,为什么传入父类型还会报错呢?
我们可以换个角度想一想,我们传入一个对象进来,可定要调用它的某个方法,但是我们如果传入的是父类,可能会没有子类的方法,但是子类一定拥有父类的方法,这样可能逻辑上就说的通了,这里是我自己的理解,如有不对大家请指出。

通过上面的代码我们可以发现,extends是为了安全的访问数据,而super是为了安全的写入数据。

7、虚拟机是如何实现泛型的

java实现泛型使用了类型擦除

public class GenericType<T> {
    private T data;

    public T getData() {
        return data;
    }

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

这段代码在虚拟机中会变成下面这样

public class GenericType<Object> {
    private Object data;

    public Object getData() {
        return data;
    }

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

也就是泛型T变成了Object,这也就是所谓的泛型擦除
如果是下面这种状况

public class GenericType<T extends ArrayList & Comparable> {
    private T data;

    public T getData() {
        return data;
    }

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

如果是上面这种情况,T则会变成ArrayList。如果使用了限定符,则会转为限定符后面的第一个类型。
如果调用了Comparable相关的方法,则会在变量前面进行强转,即下面这种情况。

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

接下来我们看下面这段代码

    public static void ListMethod(List<String> list){
        
    }

    public static void ListMethod(List<Integer> list){

    }

我们知道方法的重载是参数列表不同,但是上面的代码编译器是会报错的,也是因为泛型擦除,即List参数类型都变为了List<Object>了。

最后我们看下泛型是怎么生效的

public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        map.put("test","aaaa");
        System.out.println(map.get("test"));
    }
//编译为class之后样式
public static void main(String[] args) {
        Map<String, String> map = new HashMap();
        map.put("test", "aaaa");
        System.out.println((String)map.get("test"));
    }

我们发现在字节码中对map.get("test")进行了强转,这也就是泛型的实现方式,强转。

泛型擦除并不是真正意义上的擦没了,泛型的类型会保存在字节码文件中,这样才实际调用的时候回进行强转。

以上就是泛型相关的一些知识了,如果有哪些不对的地方,希望大家指出,共同进步。

相关文章

  • Java泛型教程

    Java泛型教程导航 Java 泛型概述 Java泛型环境设置 Java泛型通用类 Java泛型类型参数命名约定 ...

  • 第二十八课:泛型

    泛型出现之前 泛型出现之后 Java深度历险(五)——Java泛型

  • Kotlin 泛型

    说起 kotlin 的泛型,就离不开 java 的泛型,首先来看下 java 的泛型,当然比较熟悉 java 泛型...

  • java泛型中类型擦除的一些思考

    java泛型 java泛型介绍 java泛型的参数只可以代表类,不能代表个别对象。由于java泛型的类型参数之实际...

  • Java泛型

    参考:Java知识点总结(Java泛型) 自定义泛型类 自定义泛型接口 非泛型类中定义泛型方法 继承泛型类 通配符...

  • Java泛型—Java语法糖,只在编译有作用,编译后擦出泛型

    Java泛型—Java语法糖,只在编译有作用,编译后擦出泛型 在代码进入和离开的边界处,会处理泛型 Java泛型作...

  • JAVA 核心笔记 || [xxx] 泛型

    泛型 JAVA 的参数化类型 称为 泛型 泛型类的设计 Learn12.java 运行

  • 简单回顾Java泛型之-入门介绍

    什么时候开始有了Java泛型?什么是Java泛型?为什么要引入Java泛型?什么时候用到了泛型?可不可以给泛型下一...

  • Kotlin 泛型

    Kotlin 支持泛型, 语法和 Java 类似。例如,泛型类: 泛型函数: 类型变异 Java 的泛型中,最难理...

  • JAVA-泛型

    JAVA-泛型 sschrodinger 2018/11/15 简介 泛型是Java SE 1.5的新特性,泛型的...

网友评论

      本文标题:Java泛型

      本文链接:https://www.haomeiwen.com/subject/yggzyktx.html