【泛型】java中“泛型”的那些事~

【JavaSE】java中“泛型”的那些事~

  泛型在java中是非常常见的技术,下面我会从3个方面介绍一下泛型,即What(什么是泛型?),Why(为什么会有泛型?),How(泛型怎么用呢?),还有泛型的一些其他知识(通配符、泛型擦除)。

1. 什么是泛型?(What)

在这里插入图片描述

  这个玩意,就是泛型…的一种

  是的,没错,只要是用尖括号括起来引用数据类型,都是泛型

  简单来说,泛型就是用尖括号括起来的引用数据类型,注意是引用**数据类型哦

  引用数据类型都有什么呢?Integer、String、Char…大写字母开头的,

  与之对应的是基本数据类型:int、char、float…都是小写字母开头的

​  ***复杂***来说,泛型(generic type)就是:泛型其本质是将类型参数化,也就是说所操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

2. 为什么会有泛型?(Why)

2.1 规范

  我们来看一段代码,这是泛型没出来之前小明同学写的一段代码

    public static void main(String[] args) {
        ArrayList list = new ArrayList();

        list.add("abc");
        list.add("abcd");
        list.add(666);

        System.out.println(list.get(0));
        System.out.println(list.get(1));
        System.out.println(list.get(2));
    }

在这里插入图片描述

  我们运行一下,发现是可以正常运行的,但是…

在这里插入图片描述

​  我们想要遍历集合获得每个元素的长度时,智能的IDEA就会给我们报错,这是因为list集合中既有Integer类型,又有String类型,我们知道length()这个方法是Sting类型的元素特有的方法,Integer类型的元素是没有的,所以编译器就会报错,那怎么解决呢?

  只要把Integer类型和String类型分开存储不就行了吗,这样我们就可以遍历集合,然后使用String类型的特有方法啦

public static void main(String[] args) {
    ArrayList listInt = new ArrayList();

    listInt.add(1);
    listInt.add(2);
    listInt.add(3);

    ArrayList listString = new ArrayList();
    listString.add("abc");
    listString.add("abcde");
    listString.add("abedefg");
    
    for (int i = 0; i < listString.size(); i++) {
        System.out.println(listString.get(i).length());
    }
}

在这里插入图片描述

​  但是,在某天写代码的时候,小明同学又不小心把Integer类型的数据添加到了String类型的集合之中了,这就导致他遍历集合取每个字符串长度的时候又发生了报错,于是小明同学就想,我要怎么才能提前知道我”不小心把Integer类型的数据添加到了String类型的集合之中“了呢,于是,泛型诞生了!!!小明同学规定,集合指定了泛型之后,只能添加一种数据类型,如果添加别的数据类型,就会报错

在这里插入图片描述

​  就像这样

  而事实上,人们定义泛型的初衷就是这样:

如果我们没有给集合指定类型,默认认为所有的数据类型都是Object类型
此时可以往集合添加任意的数据类型。
带来一个坏处:我们在遍历获取数据的时候,无法使用他的特有行为。

  所以

我们推出了泛型,可以在添加数据的时候就把类型进行统一。
而且我们在获取数据的时候,也省的强转了,非常的方便。

2.2 实用

  还有一方面,我们在学数据结构时,几乎都手动实现了ArrayList…了吧…(是吧…)

  下面将用两种方式实现ArratList:用泛型和不用泛型

1. 不用泛型
public class MyArrayList {
    private int[] array;
    private int size;    // 有效数据个数

    public MyArrayList() {
        this.array = new int[10];
        this.size = 0;
    }

    public void add(int x) {    // 暂不考虑扩容
        this.array[size] = x;
        this.size++;
    }
}

  如果我们想使用我们自己的ArrayList

public static void main(String[] args) {
    MyArrayList myArrayList = new MyArrayList();
    myArrayList.add(1);
    myArrayList.add(2);
    myArrayList.add(3);

    System.out.println(myArrayList);
}

  可以说是十分完美啊,但是

  如果我想创建一个存储字符串的集合呢?

在这里插入图片描述

  又报错了!!!原因很简单,我们自己定义的ArrayList,存储数据的类型是int,只能存储int型的数据,你要存储String类型的数据,不报错才怪

  小明同学说了,那我就要存储String类型的数据,怎么解决呢?很简单,我们重新实现一个String类型的ArrayList不就行了吗?

  小明同学又说了,那我还要存储flaot类型的数据。也很简单,我们再重新实现一个flaot类型的ArrayList不就行了吗?

  那我还要存储short类型的数据,

​  我还要存储short类型的数据,

  我还要存储char类型的数据…

  额。。。。。。

2. 用泛型

  为了不那么麻烦的解决小明同学的问题,(泛型诞生了!!!)*2

public class MyArrayList<E> {
    // 在类的实现中,可以直接将类当成一种数据类型来使用。在实例化该类的时候这个类型才被确定
    private E[] array;
    private int size;   // 有效数据个数
    public MyArrayList2() {
        this.array = (E[])new Object[10];   // 注意:Java中泛型不允许定义数组
        this.size = 0;
    }
    public void add(E e) {    // 不考虑扩容
        this.array[size] = e;
        this.size++;
    }
}
// 带泛型的顺序表元素类型是一个“变量”
// E就是变量的名称

  来看这段代码,我们只实现一种ArrayList,但是在定义中不明确指定要存储的数据类型,而是用代替,这就是大名鼎鼎的:泛型!!!,这样,我们只需要在new一个ArrayList的时候,传入要存储数据的类型,就可以做到一种实现,多种使用了嘻嘻…

  可以说是非常的实用呢…

2.3 总结

  所以,为什么会有泛型的出现呢?主要就是两种原因:

1.如果我们没有给集合指定类型,默认认为所有的数据类型都是Object类型
  此时可以往集合添加任意的数据类型。
  带来一个坏处:我们在遍历集合获取数据的时候,无法使用他的特有行为
  所以,
  人们推出了泛型,可以在添加数据的时候就把类型进行统一规范
  而且我们在获取数据的时候,也省的强转了,非常的方便
  
2.我们定义数据结构时,要想该数据结构能够存储所有的数据类型,就要使用到泛型
  在定义数据结构时,不明确指定要存储的数据类型,而是用<E>代替
  在创建该数据结构时,再将数据类型传入进来
  这样,我们就可以定义一种数据结构来存储多种数据类型的数据了

3. 泛型怎么用呢(How?)

  说了这么多,小明同学早就迫不及待了:”那泛型到底有什么用啊???“,泛型的主要应用有3种:泛型类、泛型方法、泛型接口

泛型类:在类名后面定义泛型,创建该类对象的时候,确定类型

泛型方法:在修饰符后面定义方法,调用该方法的时候,确定类型

泛型接口:在接口名后面定义泛型,实现类确定类型

3.1 泛型类

  当我们在编写一个类的时候,如果不确定类中要存储数据的类型,那么这个类就可以定义为泛型类。

  我们上面定义的“实用型”MyArrayList,就是一种泛型类

public class MyArrayList2<E> {
    // 在类的实现中,可以直接将类当成一种数据类型来使用。在实例化该类的时候这个类型才被确定
    private E[] array;
    private int size;   // 有效数据个数
    public MyArrayList2() {
        this.array = (E[])new Object[10];   // 注意:Java中泛型不允许定义数组
        this.size = 0;
    }
    public void add(E e) {    // 不考虑扩容
        this.array[size] = e;
        this.size++;
    }
}
// 带泛型的顺序表元素类型是一个“变量”
// E就是变量的名称

​  即在定义类的时候,不明确指定成员变量的参数,而使用(、等等都是可以的哦)代替,在实例化时,再指定具体的类型参数

public static void main(String[] args) {
	//泛型类的使用
    MyArrayList<String> myArrayList = new MyArrayList();

    myArrayList.add("a");
    myArrayList.add("b");
    myArrayList.add("c");

    System.out.println(myArrayList);
}

3.2 泛型方法

  当我们在编写一个方法的时候,如果不确定方法操作的数据的类型,那么这个方法就可以定义为泛型方法 。

  例如:我们定义一个工具类ListUtil,类中定义一个静态方法addAll,用来添加多个集合的元素。

public class ListUtil {
    private ListUtil(){}

    //类中定义一个静态方法addAll,用来添加多个集合的元素。

    /*
    *   参数一:集合
    *   参数二:要添加的元素
    *
    * */
    public static<E> void addAll(ArrayList<E> list, E e){
        list.add(e);
    }
}

  我们可以调用addAll()方法给String类型的集合添加元素,也可以给Integer类型的数组添加元素

public static void main(String[] args) {
    ArrayList<String> listString = new ArrayList<String>();

    ArrayList<Integer> listInteger = new ArrayList<Integer>();
    
    ListUtil.addAll(listString, "a");
    ListUtil.addAll(listString, "b");
    ListUtil.addAll(listString, "c");
    System.out.println(listString);
    
    ListUtil.addAll(listInteger, 1);
    ListUtil.addAll(listInteger, 2);
    ListUtil.addAll(listInteger, 3);
    System.out.println(listInteger);
}

3.3 泛型接口

  泛型接口是指接口的定义中使用泛型类型参数。与泛型类类似,接口的泛型参数可以在实现接口的类中指定。这样可以让接口更加灵活,能够适应不同的数据类型。

  来看下面这段代码

// 定义一个泛型接口
interface Pair<K, V> {
    K getKey();
    V getValue();
}

// 实现泛型接口
class KeyValuePair<K, V> implements Pair<K, V> {
    private K key;
    private V value;
    
    public KeyValuePair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public K getKey() {
        return key;
    }

    @Override
    public V getValue() {
        return value;
    }

}

public class Main {
    public static void main(String[] args) {
        // 创建一个键值对,键是 String 类型,值是 Integer 类型
        Pair<String, Integer> pair1 = new KeyValuePair<>("Age", 30);
        System.out.println(pair1.getKey() + ": " + pair1.getValue());  // 输出:Age: 30
        
        // 创建一个键值对,键是 Integer 类型,值是 String 类型
        Pair<Integer, String> pair2 = new KeyValuePair<>(30, "Age");
        System.out.println(pair2.getKey() + ": " + pair2.getValue());  // 输出:30:Age
    }
}

  我们可以通过泛型接口,实现不同类型的键值对创建,可以是(String,Integer),也可以是(Integer,String)

4. 泛型通配符(了解即可)

  在 Java 中,泛型通配符是一种特殊的符号,通常用于指定泛型的类型参数范围。通配符可以让你编写更通用、灵活的代码,尤其是在涉及到泛型的集合操作时,它使得方法可以接受不同类型的参数。Java 提供了三种主要的泛型通配符:?(无界通配符)、? extends T(上界通配符)和 ? super T(下界通配符)。

在这里插入图片描述

4.1 无界通配符

  无界通配符表示可以接受任何类型。它通常用于当我们不关心具体的类型时,但需要泛型参数的某些特定行为时。

import java.util.List;

public class WildcardExample {
    public static void printList(List<?> list) {
        for (Object item : list) {
            System.out.println(item);
        }
    }
}
    
public static void main(String[] args) {
    List<Integer> intList = List.of(1, 2, 3);
    List<String> strList = List.of("A", "B", "C");

    // 可以接受任何类型的 List
    printList(intList);  // 输出:1 2 3
    printList(strList);  // 输出:A B C
}
  • List<?> 是无界通配符,它表示可以是任何类型的 List。
  • 使用无界通配符时,无法调用特定类型的方法(如 add()),只能读取对象并转为 Object 类型。

4.2 上界通配符

  上界通配符用于表示“某个类型的子类型”,即泛型的类型参数必须是 TT 的某个子类。这通常用于只读的操作,能够确保我们可以安全地访问对象的值,但不能修改它们。

import java.util.List;

public class UpperBoundWildcardExample {
    // 使用上界通配符,表示 List 中的元素是某个类型的子类
    public static void printNumbers(List<? extends Number> list) {
        for (Number number : list) {
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3);
        List<Double> doubleList = List.of(1.1, 2.2, 3.3);

        printNumbers(intList);   // 输出:1 2 3
        printNumbers(doubleList);  // 输出:1.1 2.2 3.3
    }

}
  • List<? extends Number> 表示这个 List 中的元素必须是 Number 类型或者 Number 的子类(如 Integer, Double 等)。
  • 由于我们使用了上界通配符,因此我们可以在方法中读取 Number 类型的元素,但不能对其进行修改(比如无法调用 add() 方法)。

4.3 下届通配符

​  下界通配符用于表示“某个类型的父类”,即泛型的类型参数必须是 TT 的某个父类。这通常用于写操作,比如往集合中添加元素。

javaCopy Codeimport java.util.List;

public class LowerBoundWildcardExample {
    // 使用下界通配符,表示可以接受 T 或 T 的父类
    public static void addNumbers(List<? super Integer> list) {
        list.add(10); // 可以安全地添加 Integer 类型
        // list.add(3.14); // 错误:不能添加 Double 类型
    }

    public static void main(String[] args) {
        List<Number> numberList = new ArrayList<>();
        addNumbers(numberList);  // 合法,因为 Number 是 Integer 的父类

        List<Object> objectList = new ArrayList<>();
        addNumbers(objectList);  // 合法,因为 Object 是 Integer 的父类

        // List<Integer> integerList = new ArrayList<>();
        // addNumbers(integerList);  // 错误:不能向 List<Integer> 中添加其他 Integer 之外的类型
    }
}
  • List<? super Integer> 表示这个 List 中的元素必须是 Integer 或 Integer 的父类(如 Number 或 Object)。
  • 我们可以在方法中安全地向列表中添加 Integer 类型的元素,但不能添加其他类型(如 Double)。

5. 泛型擦除

​  java中的泛型是伪泛型。Java 的泛型只存在于编译阶段。

  如下图,编译时期(java文件),调用list.add()方法时编译器会对传入的参数进行类型检查,如果传入的参数不是String类型的话,编译器会报错,但是编译完生成class文件后,list方法的类型会变成Object类型(所有类型的父类或间接父类),不会保留String泛型的信息。
在这里插入图片描述

  在 Java 中,泛型是编译时的特性,它通过类型参数化提供了类型安全和灵活性。然而,Java 在运行时并不保留泛型的类型信息,所有泛型的类型信息都会被“擦除”,这就是所谓的泛型擦除(Generic Type Erasure)。泛型擦除的目的是为了兼容 Java 的旧版本(即 Java 1.5以前的版本),从而实现向后兼容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值