预备知识---java之泛型的探索

简介: 预备知识---java之泛型的探索

泛型(了解掌握)

在之前的学习中,我们曾自己实现过一个顺序表,如果想要温习的同学可以直接来我这篇博客进行翻阅:附上博客链接:

点击此处进入博客

之前我们对于顺序表的实现也是只能插入整形,不能插入其他类型的数据,那么现在假如我们想要实现一个顺序表是可以插入任何数据的,那么该怎样进行实现呢?来看我们的代码:我们只实现两个方法来表达我们想说的意思

class MyArrayList1 {
    //定义一个Object类型的数组,用于存储所有类型的数据.
    //Object类是所有类的父类,即使这个类他不继承Object类
    public Object[] elem;
    public int usedSize;
   //实例化的时候生成一个大小为10的数组
    public MyArrayList1() {
        this.elem = new Object[10];
    }
   //数据的类型也替换成Object,这样才能保证插入所有类型数据
    public void add(Object data) {
        this.elem[this.usedSize] = data;
        usedSize++;
    }
    public Object get(int pos) {
        return this.elem[pos];
    }
}
public class TestDemo {
    public static void main(String[] args) {
        MyArrayList1 myArrayList = new MyArrayList1();
        //什么类型都可以放到数组里
        myArrayList.add(19);
        myArrayList.add(20);
        myArrayList.add("sdsd");
        //缺点:取数据的时候需要强制类型转换
        int a=(int)myArrayList.get(1);
        System.out.println(a);
    }
}

可以看到只需要修改数据类型为Object,便可以完成对顺序表的操作,但是这样的操作有一个缺点,就是插入数据的时候可以随便插入,但是获取数据的时候却必须进行强制类型转换,所以就会显的非常的麻烦,此时为了简化就需要用到泛型.

下面先来看下泛型是如何解决我们上述顺序表的,来看源码(并仔细看注释):

//类这块的泛型的字母可以是任意的
//T是类型变量(Type Variable),变量名一般要大写
//T在定义时是形参,代表的意思是MyArrayList最终传入的类型,但现在还不知道
class MyArrayList<T> {
    //定义一个T类型的数组,此时并不知道到底是什么类型的数组
    public T[] elem;
    public int usedSize;
    public MyArrayList() {
        /*此处为什么要用强制类型转换:我来解释下:
        首先这么写是因为泛型类的原因,当我们强转其为T类型的数组时,此时我们并不知道这个数组强转后到底是什么数组,因为
        T此时并没有给定一个合适的引用类型,而数组的类型是由后续我们填入的引用类型来决定的,这就提供类一个通用的数组模板,且后期
        不需要进行强制类型转换
        */
        this.elem = (T[]) new Object[10];
        /*为什么不直接定义一个T类型的数组,因为此时发生了泛型的擦除机制,即将泛型擦除为Object,从而此时的泛型具有了Object的特质
        所以此时的this.elem=new T[10];就等价于this.elem=new Object[10];
        注意我们并不能直接写成this.elem=new T[10]这样的形式,原因是T只是在编译的时候被擦除为Object,具有了Object的特质,并不是T直接就等价于Object
        而当我们是想要一个非Object类型的通用的数组,且后期不需要进行强制类型转换,此时才需要写成 this.elem = (T[]) new Object[10]这种形式.
        并且只有当父类赋给子类的时候才进行强制类型转换,子类给父类不需要进行强制类型转换,因为发生了向上转型.
        /*
        此时大家还是会有疑问,此时创建数组可否换一个写法:如下所示:
         this.elem = (T[]) new Integer[10];
         此时我们会发现编译器会报出一个ArrayStoreException异常,原因是T与Object其实在这里是绑定的,
         举个例子,假如我们此时T处为String类型的话,(T[]) new Object[10];就等价于(String[]) new Object[10],
         此时String是Object类的子类,此时便会创造出一个String类型的数组,
         如果此时是(T[]) new Integer[10];这段代码的话,就等价于(String[]) new Integer[10],此时String并不是Integer的子类
         那么最终便会抛出ArrayStoreException异常。
         */
    }
    //插入数据,插入的数据类型由T处的数据类型决定
    public void add(T data) {
        this.elem[this.usedSize] = data;
        usedSize++;
    }
    //根据T处的数据类型来返回相应的值
    public T get(int pos) {
        return this.elem[pos];
    }
}
public class TestMain {
    public static void main1(String[] args) {
        //T为String类型
        MyArrayList<String> myArrayList = new MyArrayList<>();
        myArrayList.add("sss");
        myArrayList.add("ddd");
        myArrayList.add("fff");
        //用了泛型后就自动进行类型转换了
        String str = myArrayList.get(1);
        System.out.println(str);
        //T为整数类型
        MyArrayList<Integer> myArrayList2 = new MyArrayList<>();
        myArrayList2.add(1);
        myArrayList2.add(2);
        int val = myArrayList2.get(1);
        System.out.println(val);
        //T为浮点数类型
        MyArrayList<Double> myArrayList3 = new MyArrayList<>();
       //假设不指定参数类型,此时默认插入的数据可以是任意类型,因为默认是Object类型
        MyArrayList myArrayList4 = new MyArrayList();
        myArrayList4.add(1);
        myArrayList4.add("sdsdsd");
        myArrayList4.add(7.9);
        //结果是sdsdsd
        System.out.println(myArrayList4.get(1));
    }
}

可以看到此时不再进行强转类型转换来获取顺序表中的数据,而是直接通过下标便可以获取,这就是泛型的厉害之处

从而引出泛型的两个意义:

1、自动进行类型的检查


为什么可以自动进行类型的检查,是这样的:T处的数据类型可以有很多种,例如简单数据类型的包装类,引用类型以及自定义数据类型,假如此时我们在泛型类内部

定义一个数组的话,这个数组的类型可以跟T处定义的类型有关,并且后期往数组里面插入数据的时候会自动检查插入的数据是否跟T中的数据类型匹配,如果匹配,那么就直接插入,这样子我们就制作出来了一个通用的顺序表


2、自动进行类型的转换


紧接着上面,根据之前我们写通用顺序表的写法,是直接定义一个Object类型的数组,这样就会导致

最终我们在获取顺序表中的某个值的时候必须进行强制类型转换(具体可参照上面的diamagnetic),而当我们使用了泛型之后,便不再存在这样的问题,因为泛型会帮我们自动进行类型的转换。例如String str = myArrayList.get(1)这段代码,他就自动进行了类型转换


同时通过初始化数组的时候的T【】这样的强转方式,我们引出一道面试题目:


泛型是怎么编译的?

答:这涉及到了泛型的擦除机制,进行类型擦除,编译的时候都会把泛型擦除为Object,并不是我们所理解的替换为Object,从而此时的泛型具有了Object的特质


泛型的注意事项

1:泛型只存在于编译时期,只是编译时期的一种机制,1.即运行期间没有泛型的概念。

2:简单类型不能做泛型类型的参数,例如下面的int就不能做参数,尖括号中只能是引用类型,而像java当中的八种基本数据类型

就不能放在尖括号里面,此时放入的应该是这八种基本类型所对应的包装类,因为包装类是引用类型

MyArrayList myArrayList1 = new MyArrayList<>();


2:泛型在编译的时候 并不会进行指定类型的替换 而是拿着指定的类型进行检查, 也就是说在编译的时候 ,拿着你指定的类型进行类型检查 ,记住我并没有说是替换

例如下面的代码:

MyArrayList myArrayList = new MyArrayList<>();

myArrayList.add(“sss”);

此时插入的时候会拿着String这个类型进行检查,如果插入的是字符串,就不会报错,否则便报错。此时并不是说我把泛型里面的参数替换成了String。


3:编译的时候 会进行类型擦除,编译的时候都会把泛型擦除为Object,并不是我们所理解的替换为Object,从而此时的泛型具有了Object的特质

这样就很好解释下面的代码为什么我们可以在<>中放入很多不同的引用类型,例如String,Interger,Double,这些,因为在编译的时候已经将泛型T擦除为Object类型,而Object是所有类的父类,所以就可以放入许多引用类型以及包装类(包装类本质上也是引用类型)


4:不能new 泛型类型的数组 this.elem = new T[10];

因为T的类型不能确定,编译和运行时候都不知道T的类型,所以不能new出来一个泛型类型的数组,只能强转。这里跟上面的擦除也有密切的联系,假如我们在编译的时候对类型是进行了替换而不是擦除,那么此处相当于将T替换成了Object类型,那么这块实例化一个T对象的时候是不会报错的,相当于

this.elem = new Object[10],正是因为编译的时候发生的是擦除而不是替换,所以此处不能像那样书写.


5:所以T就是个模板,里面可以放不同的引用类型。

同时T代表占位符,表示当前的类是一个泛型类,泛型的标志就是尖括号<>

尖括号和T的作用就是帮助我们进行类型的检查与类型的转换,例如在插入数据的时候判断是否类型符合.

6:泛型类可以一次有多个类型变量,用逗号分割,例如

7:泛型主要包含:泛型类、泛型方法和泛型接口


再次证明泛型是编译时期的一种机制

package Genetic;
class Person{
}
public class TestMain {
    public static void main(String[] args) {
        Person person = new Person();
        //输出结果为Genetic.Person@4554617c
        System.out.println(person);
        MyArrayList<String> myArrayList1 = new MyArrayList<>();
        //输出结果为Genetic.MyArrayList@74a14482
        System.out.println(myArrayList1);
        MyArrayList<Integer> myArrayList2 = new MyArrayList<>();
        MyArrayList<Double>  myArrayList3 = new MyArrayList<>();
        //输出结果为Genetic.MyArrayList@1540e19d
        System.out.println(myArrayList2);
        //输出结果为Genetic.MyArrayList@677327b6
        System.out.println(myArrayList3);
          }
 }

此时我们在Person类中没有重写toString方法,所以最终我们的输出结果的组成形式为包名+类+@+存储对象地址的哈希值.当然我们可以看到当我们使用泛型的时候,运行后打印的值中是没有泛型中的值的,说明泛型类型的参数不参与类型的组成,更加验证了泛型只存在于编译时期这一观点.


泛型类的定义-类型边界(泛型上界)

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。

class 泛型类名称<E extends U> {
...
}

在实例化时,E只能是U的子类,否则编译会报错


泛型上界的第一个小例子

注意下面的代码告诉我们将来对MyArray进行实例化时,实例化的类型必须要是Animal的子类才可以

/**
 * @author SongBiao
 * @Date 15:36
 */
// 将来对MyArray进行实例化时,实例化的类型必须要是Animal的子类才可以
public class MyArray<E extends Animal> {
    private E[] array = null;
    private int size;
    private int capacity;
    public MyArray(int capacity) {
        array = (E[]) new Object[capacity];
        size = 0;
        this.capacity = capacity;
    }
    public static void main(String[] args) {
        // 编译成功,因为Dog是Animal的子类MyArray<Dog> m1 = new MyArray<>(10); m1.add(new Dog("旺财"));
        m1.add(new Dog("二哈"));
       // 编译成功,因为Cat是Animal的子类MyArray<Cat> m2 = new MyArray<>(10); m2.add(new Cat("肥波"));
        m2.add(new Cat("加菲"));
        // 编译失败,因为String不是Animal的子类
        MyArray<String> m3 = new MyArray<>(10);
    }
}

注意:没有指定类型边界 E,可以视为 E extends Object


泛型上界的第二个小例子

要求:此时写一个泛型类,找到数组当中的最大值


//此处泛型中继承了我们的Comparable接口,T extends Comparable<T>这种行为我们称之为泛型上界
//并且因为在下面我们要实现对于引用类型的比较,所以要继承Comparable接口
class Algorithm<T extends Comparable<T>> {
    public T findMaxVal(T[] array) {
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            //使用compareTo方法进行比较,因为是引用类型的比较大小
            //如果array[i]>max,compareTo方法的返回值为1
            if (array[i].compareTo(max) >= 0) {
                max = array[i];
            }
        }
        return max;
    }
}
public class TestMain {
    public static void main(String[] args) {
        //注意此处为包装类Integer
        Integer[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9};
        Algorithm<Integer> algorithm = new Algorithm<>();
        //获取最大的数值
        Integer a = algorithm.findMaxVal(array);
        //输出最大值9
        System.out.println(a);
    }
}

泛型方法

假如我们对上面的代码进行改写,直接拿类去调用findMaxVal方法的话,那么此时就需要我们的泛型方法了

/*
写一个泛型类,找到数组当中的最大值
*/
//如果要类直接调用的话,只需要将这个方法修改为静态方法即可
class Algorithm {
    //泛型方法
    public static<T extends Comparable<T>> T findMaxVal(T[] array) {
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            //如果array[i]>max,compareTo方法的返回值为1
            if (array[i].compareTo(max) >= 0) {
                max = array[i];
            }
        }
        return max;
    }
}
public class TestMain {
    public static void main(String[] args) {
        //注意此处为包装类Integer
        Integer[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9};
        //获取最大的数值
        Integer a =Algorithm.findMaxVal(array);
        //输出最大值为9
        System.out.println(a);
    }
}

注意事项:

泛型没有上界的时候,我们的泛型要这样写:

public static T findMaxVal(T[] array){}

泛型有上界的时候,我们的泛型要这样写:

public static> T findMaxVal(T[] array) {}


泛型是没有下界的!!!

通配符

再来复习一下泛型方法:来看一个例子


需求:写一个方法,打印一个list当中的所有的数据

import java.util.ArrayList;
class Algorithm {
    public static<T> void print(ArrayList<T> list) {
        for (T x:list) {
            System.out.println(x);
        }
    }
}
public class TestMain {
    public static void main(String[] args) {
        ArrayList<Integer> list1 = new ArrayList<>();
        list1.add(1);
        list1.add(2);
        list1.add(3);
        Algorithm.print(list1);
        ArrayList<Double> list2 = new ArrayList<>();
        list2.add(1.0);
        list2.add(2.0);
        list2.add(3.0);
        Algorithm.print(list2);
    }
}

基本概念

? 用于在泛型的使用,即为通配符,通配符多在源码中出现.


假设此时我们修改上述代码:使用通配符来写上述代码该怎么写呢?下面来看代码

import java.util.ArrayList;
class Algorithm {
    //修改为通配符?
    public static void print(ArrayList<?> list) {
        //注意通配符的意思就是根本不知道用户要传的参数是什么,所以此处就用所有类的父类Object进行遍历
        for (Object x:list) {
            System.out.println(x);
        }
    }
}
public class TestMain {
    public static void main(String[] args) {
        ArrayList<Integer> list1 = new ArrayList<>();
        list1.add(1);
        list1.add(2);
        list1.add(3);
        Algorithm.print(list1);
        ArrayList<Double> list2 = new ArrayList<>();
        list2.add(1.0);
        list2.add(2.0);
        list2.add(3.0);
        Algorithm.print(list2);
    }
}

那么泛型T跟这个通配符?到底有什么区别呢?


因为是两个顺序表的插入和获取,所以我们来看泛型和通配符在插入和获取的时候的区别吧

通配符的插入在插入的时候根本不知道是什么类型的插入

而获取的时候是通过下标值来获取对应元素

而泛型的插入在插入的时候的类型是泛型类型T

获取的时候同样是通过下标值来获取对应元素

2.png

所以最终总结下来就是:

通配符和泛型的作用是相同的,但是应用场景有很大的不同:


泛型一般用于读取和写入

通配符一般用于读取


通配符上界

语法

<? extends 上界E>
其中?表示传入的参数,其范围是上界E的子类或者上界E其本身

示例

2.png


注意: 需要区分 泛型使用中的通配符上界 和 泛型定义中的类型上界


通配符下界

语法

<? super 下界E>
其中?表示传入的参数,其范围是下界E的父类或者下界E自己本身

示例

2.png

相关文章
|
4月前
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
89 2
|
2月前
|
Java API
[Java]泛型
本文详细介绍了Java泛型的相关概念和使用方法,包括类型判断、继承泛型类或实现泛型接口、泛型通配符、泛型方法、泛型上下边界、静态方法中使用泛型等内容。作者通过多个示例和测试代码,深入浅出地解释了泛型的原理和应用场景,帮助读者更好地理解和掌握Java泛型的使用技巧。文章还探讨了一些常见的疑惑和误区,如泛型擦除和基本数据类型数组的使用限制。最后,作者强调了泛型在实际开发中的重要性和应用价值。
30 0
[Java]泛型
|
2月前
|
存储 安全 Java
🌱Java零基础 - 泛型详解
【10月更文挑战第7天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
18 1
|
2月前
|
Java 语音技术 容器
java数据结构泛型
java数据结构泛型
27 5
|
3月前
|
Java 编译器 容器
Java——包装类和泛型
包装类是Java中一种特殊类,用于将基本数据类型(如 `int`、`double`、`char` 等)封装成对象。这样做可以利用对象的特性和方法。Java 提供了八种基本数据类型的包装类:`Integer` (`int`)、`Double` (`double`)、`Byte` (`byte`)、`Short` (`short`)、`Long` (`long`)、`Float` (`float`)、`Character` (`char`) 和 `Boolean` (`boolean`)。包装类可以通过 `valueOf()` 方法或自动装箱/拆箱机制创建。
43 9
Java——包装类和泛型
|
2月前
|
存储 Java 编译器
Java集合定义其泛型
Java集合定义其泛型
19 1
|
2月前
|
存储 Java 编译器
【用Java学习数据结构系列】初识泛型
【用Java学习数据结构系列】初识泛型
22 2
|
3月前
|
安全 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版)
|
3月前
|
存储 安全 搜索推荐
Java中的泛型
【9月更文挑战第15天】在 Java 中,泛型是一种编译时类型检查机制,通过使用类型参数提升代码的安全性和重用性。其主要作用包括类型安全,避免运行时类型转换错误,以及代码重用,允许编写通用逻辑。泛型通过尖括号 `&lt;&gt;` 定义类型参数,并支持上界和下界限定,以及无界和有界通配符。使用泛型需注意类型擦除、无法创建泛型数组及基本数据类型的限制。泛型显著提高了代码的安全性和灵活性。
|
2月前
|
安全 Java 编译器
Java基础-泛型机制
Java基础-泛型机制
17 0