【Java】泛型

IsLand1314~ 2024-10-25 10:35:02 阅读 97

 📃个人主页:island1314

⛺️  欢迎关注:👍点赞 👂🏽留言 😍收藏  💞 💞 💞


1. 什么是泛型

🍊 泛型(Generics)是Java编程语言中的一个强大的特性,它提供了 编译时类型安全检测机制,这意味着可以在编译期间检测到非法的类型。泛型的使用减少了程序中的强制类型转换和运行时错误的可能性

因为在一般的类和方法,只能使用具体的类型:

要么是基本类型要么是自定义的类。

因此如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。因此就在JDK1.5 引入的新的语法 --- 泛型

通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化

2. 引出泛型

🍎 现在有个问题,我们如果想要实现一个类,类中包含一个数组成员,然后可以使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值,该怎么去做呢?

思路:

 我们以前学过的数组,只能存放指定类型的元素,例如:int[] array = new int[10]; String[] strs = new String[10] ; 所有类的父类,默认为Object类。数组是否可以创建为Object?

<code>class MyArray {

public Object[] array = new Object[10];

public Object getPos(int pos) {

return this.array[pos];

}

public void setVal(int pos, Object val) {

this.array[pos] = val;

}

}

public class TestDemo {

public static void main(String[] args) {

MyArray myArray = new MyArray();

myArray.setVal(0, 10);

myArray.setVal(1, "hello");//字符串也可以存放

String ret = myArray.getPos(1);//编译报错

System.out.println(ret);

}

}

问题:以上代码实现后 发现

 任何类型数据都可以存放 1号下标本身就是字符串,但是确编译报错。必须进行强制类型转换

虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能够持有一种数据类型。而不是同时持有这么多类型

所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译器去做检查。此时,就需要把类型作为参数传递。需要什么类型,就传入什么类型。

3. 泛型类 🔖

3.1 泛型类语法 

🌈 定义类时,在类名后加上用尖括号括起来类型形参,这个类就是泛型类

创建泛型类的实例对象时传入不同的类型实参,就可以动态生成任意多个该泛型类的子类在JDK类包中泛型类最典型的应用就是各种容器类,如ArrayList、HashMap等

定义泛型类:

class 泛型类名称 < 类型形参列表 > {

// 这里可以使用类型参数

}

class 泛型类名称 < 类型形参列表 > extends 继承类 /* 这里可以使用类型参数 */ {

// 这里可以使用类型参数

}

实例化:

泛型类 < 类型实参 > 变量名 ; // 定义一个泛型类引用

new 泛型类 < 类型实参 > ( 构造方法实参 ); // 实例化一个泛型类对象

💢上述语法格式中,类名<类型形参变量>是一个整体的数据类型,通常称为泛型类型;类型形参变量没有特定的意义,可以是任意一个字母,但是为了提高可读性,建议使用有意义的字母。一般情况下使用得较多的字母及意义如下:

E:表示 Element(元素),常用在Java Collection中,如 List<E>、Iterator<E>、Set<E>K,V:表示 Key和Value(Map的键值对)N:表示Number(数字)T:表示 Type(类型),如String、Integer等

🍒 定义泛型类时,类的构造方法名称还是类的名称。类型形参变量可以用于属性的类型方法的返回值类型和方法的参数类型。

💦 创建泛型类的对象时,不强制要求传人类型实参

如果传入类型实参,类型形参会根据传人的类型实参做相应的限制,此时泛型才会起到应有的限制作用;如果不传入类型实参,在泛型类中使用类型形参的方法或成员变量定义的类型可以为任何类型。

3.2 示例

MyArray<Integer> list = new MyArray<Integer>();

 注意:泛型只能接受类,所有的基本数据类型必须使用包装类!

下面对之前代码进行改进后:

class MyArray<T> {

  public T[] array = (T[])new Object[10];//1

  public T getPos(int pos) {

    return this.array[pos];

 }

  public void setVal(int pos,T val) {

    this.array[pos] = val;

 }

}

public class TestDemo {

  public static void main(String[] args) {

    MyArray<Integer> myArray = new MyArray<>();//2

    myArray.setVal(0,10);

    myArray.setVal(1,12);

    int ret = myArray.getPos(1);//3

    System.out.println(ret);

    myArray.setVal(2,"Island1314");//4

 }

}

代码解释:

 类名后的 <T> 代表占位符,表示当前类是一个泛型类注释 1 处:不能new泛型类型的数组

意味着:

T[] ts = new T[5];//是不对的 注释 2 处:类型后加入 <Integer> 指定当前类型注释 3 处:不需要进行强制类型转换注释 4 处:代码编译报错,此时因为在注释2处指定类当前的类型,此时在注释4处,编译器会在存放元素的时候帮助我们进行类型检查。

 🔥 同理,我们如果要使用 集合 来存储什么类型,就可以在创建 集合 时传入对于类型即可,如下代码:

public static void main(String[] args) {

// 只创建一个只保存 Integer 类型的 List 集合

List<Integer> intList = new ArrayList<Integer>();

intList.add(1);

// 下面的代码就会编译报错

//intList.add('2');

for(int i = 0; i < intList.size();i++) {

// 无需进行强制类型转换的事情

Integer num = intList.get(i);

}

}

3.3 类型推导(Type Inference)

当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写

MyArray<Integer> list = new MyArray<>(); // 可以推导出实例化需要的类型实参为 Integer

3.4 自定义泛型类演示

// 定义泛型类 Goods

class Goods<T>{

// 类型形参变量作用于属性的类型

private T info;

// 类型形参变量作用于构造方法的参数类型

public Goods(T info) {

this.info = info;

}

// 类型形参变量作用于方法的参数类型

public void setInfo(T info) {

this.info = info;

}

// 类型形参变量作用于方法的返回值类型

public T getInfo() {

return this.info;

}

}

public class Example {

public static void main(String[] args) {

// 创建 Goods 对象

Goods goods = new Goods<Integer>(666);

Goods goods2 = new Goods<>(123); //类型推导

// getclass 方法 是用来获得对象的运行时类

System.out.println(goods.getInfo() + "..." + goods.getInfo().getClass());

goods.setInfo("热卖商品"); // 传入实参为 String 类型

System.out.println(goods.getInfo() + "..." + goods.getInfo().getClass());

}

}

// 输出:

666...class java.lang.Integer

热卖商品...class java.lang.String

由上面的输出,我们可以看出:属性 info 的值为 666 时,类型为Integer,当 setInfo("热卖商品") 之后,此时属性 info 的类型为 String

📑 因此我们可以得到一个结论:类型形参 会根据 类型实参 来确定

4. 泛型是如何编译的❓

4.1 擦除机制

那么,泛型到底是怎么编译的

通过命令:javap -c 查看字节码文件,所有的T都是Object

 在编译的过程当中,将所有的 T 替换为  Object 这种机制,我们称为:擦除机制

Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。

4.2 泛型类型数组为什么不能实例化

<code>class MyArray<T> {

public T[] array = (T[])new Object[10];

public T getPos(int pos) {

return this.array[pos];

}

public void setVal(int pos, T val) {

this.array[pos] = val;

}

public T[] getArray() {

return array;

}

}

public class Example {

public static void main(String[] args) {

MyArray<Integer> myArray = new MyArray<>();

Integer[] num = myArray.getArray();

}

}

// 输出

Exception in thread "main" java.lang.ClassCastException:

class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.Integer;

([Ljava.lang.Object; and [Ljava.lang.Integer;

are in module java.base of loader 'bootstrap')at Example.main(Example.java:65)

原因:替换后的方法为:将Object[]分配给Integer[]引用,程序报错。

public Object[] getArray() {

  return array;

}

⭐ 通俗讲就是:返回的Object数组里面,可能存放的是任何的数据类型,可能是String,可能是Person,运行的时候,直接转给Integer类型的数组,编译器认为是不安全的。

正确的方式:【了解即可】

class MyArray<T> {

public T[] array;

public MyArray() {

}

 /**

  * 通过反射创建,指定类型的数组

  * @param clazz

  * @param capacity

  */

public MyArray(Class<T> clazz, int capacity) {

array = (T[])Array.newInstance(clazz, capacity);

}

public T getPos(int pos) {

return this.array[pos];

}

public void setVal(int pos, T val) {

this.array[pos] = val;

}

public T[] getArray() {

return array;

}

}

5.  泛型接口 🎐

5.1 概述

🎒 定义泛型接口和定义泛型类的语法格式类似,在接口名称后面加上用尖括号括起来类型形参即可。与集合相关的很多接口也是泛型接口,如:Collection、List等。

义泛型接口的基本语法格式如下:

【访问权限】interface 接口名称 <类型形参变量> ()

按照上面格式定义一个泛型接口,如下:

interface Info<T>{

public T getVar();

}

泛型接口定义完成后,就可以定义泛型接口的实现类了。泛型接口的实现类有两种定义方式:

使用非泛型类实现泛型接口使用泛型类实现泛型接口

5.2 泛型类实现泛型接口

🍉 当使用非泛型类实现接口时,需要明确接口的泛型类型,也就是需要将类型实参传入接口

此时实现类重写接口中使用泛型的地方,都需要将类型形参替换成传入的类型实参,这样就可以直接使用泛型接口的类型实参,具体如下所示。

定义一个泛型接口,如下文件 Inter.java:。

public interface Inter<T> {

public abstract void show(T t);

};

定义泛型接口的实现类,在泛型接口后指定类型实参以明确接口的泛型类型如下文件Interimpl.java

public class InterImpl implements Inter<String>

{

@Override

public void show(String s) {

System.out.println(s);

}

};

在上面代码中,在接口后面传入的类型实参类型为String。这样,在 InterImpl 实现类中重写Inter 接口中的 show() 方法时,就需要指明 show() 方法的参数类型为String

定义好泛型接口和泛型接口的实现类后,对泛型接口及泛型接口实现类的使用进试,此时创建 Inter 对象时传入的类型实参必须是String类型,否则会出现编译时异常

public static void main(String[] args) {

Inter<String> inter = new InterImpl();

inter.show("Hello Island1314");

}

// 输出:

Hello Island1314

5.3 非泛型类实现泛型接口

当使用泛型类实现泛型接口时,需要将泛型的声明加在实现类中,并且泛型类和泛型接口使用的必须是同一个类型形参变量,否则会出现编译时异常。

下面修改Interimpl.java文件,使用泛型类实现泛型接口,修改后的代码如下所示。

public class InterImpl<T> implements Inter <T> {

@Override

public void show(T t) {

System.out.println(t);

}

}

重新编辑上述 main 方法,创建 Inter 对象时传入不同的类型实参,并且分别调用 show() 方法进行输出验证,如下:

public static void main(String[] args) {

Inter<String> inter = new InterImpl<>();

inter.show("IsLand~");

Inter<Integer> i = new InterImpl<>();

i.show(1314);

}

// 输出如下:

IsLand~

1314

6. 泛型上界

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

6.1 语法

class 泛型类名称<类型形参 extends 类型边界> {

 ...

}

6.2 示例

案例一:

public class MyArray<E extends Number> {

 ...

}

只接受 Number子类作为 E 的类型实参

MyArray<Integer> l1; // 正常,因为 Integer 是 Number 的子类型

MyArray<String> l2; // 编译错误,因为 String 不是 Number 的子类型

// 报错如下:

error: type argument String is not within bounds of type-variable E

   MyArrayList<String> l2;

         ^

where E is a type-variable:

   E extends Number declared in class MyArrayList

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

案例二:

public class MyArray<E extends Comparable<E>> {

 ...

}

E 必须是实现了Comparable接口的

7. 泛型方法

7.1 概述

🌈泛型方法是将类型形参的声明放在修饰符和返回值类型之间的方法。在Java程序中,

定义泛型方法常用的格式如下:

[访问权限修饰符] [static] [final] <类型形参> 返回值类型 方法名 (形参列表) {

...

}

定义泛型方法时候,需要注意以下 6 点:

访问权限修饰符(包括private、public、protected)、static 和 final都必须写在类型形参的前面返回值类型必须写在类型形参的后面泛型方法可以在用泛型类中,也可以用在普通类中泛型类中的任何方法本质上都是泛型方法,所以在实际使用中很少会在泛型类中用上面的形式定义泛型方法。类型形参可以用在方法体中修饰局部变量,也可以修饰方法的返回值泛型方法可以是实例方法(没有用static修饰,也叫非静态方法),也可以是静态方法

泛型方法也能提高代码的重用性和程序的安全性。Java语言的编程原则是尽量设计泛型方法解决问题,如果设计泛型方法可以取代整个类的泛型化,就应该优先采用泛型方法。

示例如下:

public class Util {

    //静态的泛型方法 需要在static后用<>声明泛型类型参数

    public static <E> void swap(E[] array, int i, int j) {

        E t = array[i];

        array[i] = array[j];

        array[j] = t;

    }

}

// 使用如下:

// 情况一:可以类型推导

Integer[] a = { ... };

swap(a, 0, 9);

String[] b = { ... };

swap(b, 0, 9);

// 情况二:不使用类型推导

Integer[] a = { ... };

Util.<Integer>swap(a, 0, 9);

String[] b = { ... };

Util.<String>swap(b, 0, 9);

7.2 泛型方法的使用

在 上面中 已经介绍了泛型方法的定义格式,下面对Java程序中如何使用泛型方法进行介绍

泛型方法的使用通常有如下两种形式:

对象名 | 类名.<类型实参>方法名 (实参列表)

对象名 | 类名.方法名 (类型实参列表)

注意:

如果泛型方法是实例方法,则需要使用对象名进行调用;如果泛型方法是静态方法,可以使用类名进行调用。

可以看出,上述两种调用泛型方法的形式的差别在于方法名之前是否显式地指定类型实参

调用时是否需要显式地指定了类型实参,要根据泛型方法的声明形式以及调用时编译器能否从实参列表中获得足够的类型信息决定如果编译器能够根据实参推断出参数类型,就可以不指定类型实参;反之则需要指定类型实参

💞 下面一个案例演示泛型方法的定义和使用

class Student{

// 静态泛型方法

public static <T> void staticMethod(T t) {

System.out.println(t + "..." + t.getClass());

}

// 泛型方法

public <T> void otherMethod(T t){

System.out.println(t + "..." + t.getClass());

}

}

public class Demo {

public static void main(String[] args) {

// 使用形式一调用静态泛型方法

Student.staticMethod("staticMethod");

// 使用形式二调用静态泛型方法

Student.<String>staticMethod("staticMethod");

Student stu = new Student();

// 形式一调用普通的泛型方法

stu.otherMethod(666);

// 形式二调用普通的泛型方法

stu.<Integer>otherMethod(666);

}

}

// 输出

staticMethod...class java.lang.String

staticMethod...class java.lang.String

666...class java.lang.Integer

666...class java.lang.Integer

我们发现形式一和 形式二的输出结果一样。说明泛型方法可以在非泛型类中定义,并且可以在调用泛型方法的时候确定泛型的具体类型。

上述结果中虽然使用形式一和形式二的输出结果一致,但是形式一需要隐式传入类型实参,不能直观地看出调用的方法是泛型方法,不利于代码的阅读和维护。因此,通常建议使用第二种形式调用泛型方法

8. 类型通配符 📚

🌈 一般情况下,创建泛型类的实例对象时,应该为泛型类传入一个类型实参,以确定该泛型类的泛型类型

有时候,使用泛型类或者接口时传递的类型实参是不确定的、使用固定的类型形参接收类型实参存在局限性,此时就可以使用类型通配符接收不同的类型实参。

8.1 概述

💢 类型通配符用一个问号(?)表示,类型通配符可以匹配任何类型的类型实参

下面用一个案例演示类型通配符的使用,如下所示

// 定义泛型类 Person

class Person<T> {

private T info;

public Person(T info) {

this.info = info;

}

public void setInfo(T info) {

this.info = info;

}

public T getInfo(){

return info;

}

}

public class Demo {

public static void main(String[] args) {

// 创建 Person 对象,传入 String 类型的类型实参

Person<?> person = new Person<String>("IsLand~");

System.out.println(person.getInfo() + "..." + person.getInfo().getClass());

// 创建 Person 对象,传入 Integer 类型的类型实参

person = new Person<Integer>(1314);

System.out.println(person.getInfo() + "..." + person.getInfo().getClass());

}

}

// 输出:

IsLand~...class java.lang.String

1314...class java.lang.Integer

可以看出,控制台成功输出了两条信息,说明泛型类 Peron 的对象接收了两种不同的类型实参

注意:如果创建 Person 对象时不使用类型通配符,而是使用知道的类型实参,则会出现编译时异常,如下:

这个时候,可能有些读者朋友们会觉得可以使用 Object  来代替类型通配符接收所有类型。

那么下面使用 Object 代替上述代码的 类型通配符,修改后也出现了编译时异常,如下:

原因:在泛型中类名和泛型的声明是一个整体类型Person<Object> 并不是 Person<Object> 的父类

因此我们可以通过使用类型通配符,来接收所有的泛型类型,而且可以不让用户随意修改

"?"的基础上又产生了两个子通配符:

? extends 类:设置通配符上限? super 类:设置通配符下限下面我们来仔细了解一下

8.2 类型通配符的限定

🧩 前面使用 类型通配符的时候,实际上是任意设置的,只要是类就可以设置。但是有时候需要对 类型通配符的使用进行限定,主要限定类型通配符的上限和下限

8.2.1 设定类型通配符的上限

🌸当使用 Person<?>时,表示泛型类Person可以接收所有类型的类型实参。但有时不想让某个泛型类接收所有类型的类型实参,只想接收指定的类型及其子类,这时可以为类型通配符设定上限。

设定 类型通配符的上限的语法格式如下:

<code><? extends 类>

案例一:

代码如下:

// 设定类型通配符的上限,此时传入的类型实参必须是 Number 类型 或者 其子类

public static void getElement(Collection<? extends Number> coll){}

public static void main(String[] args) {

// 创建 Collection 对象,传入 Number 类型的类型实参

Collection<Number> l1 = new ArrayList<Number>();

// 创建 Collection 对象,传入 Integer 类型的类型实参

Collection<Integer> l2 = new ArrayList<Integer>();

// 创建 Collection 对象,传入 String 类型的类型实参

Collection<String> l3 = new ArrayList<String>();

getElement(l1);

getElement(l2);

getElement(l3);

}

代码分析:

定义了方法 getElement(),设定类型通配符的上限为 Number,设定后调用该方法时传入的类型实参必须是 Number类型或其子类后面创建了3个Collection 对象,分别传入了Number 类型、Integer 类型和String类型的类型实参。然后分别以上面创建的3个 Collection 对象参数调用 getElement()方法,由于 l3 在创建时传入的 String类型不是Number的子类,出现编译时异常

案例二:

<code>class Food {}

class Fruit extends Food {}

class Apple extends Fruit {}

class Banana extends Fruit {}

class Message<T> { // 设置泛型

private T message;

public T getMessage() {

return message;

}

public void setMessage(T message) {

this.message = message;

}

}

public class Demo {

public static void main(String[] args) {

Message<Apple> message = new Message<>();

message.setMessage(new Apple());

fun(message);

Message<Banana> message2 = new Message<>();

message2.setMessage(new Banana());

fun(message2);

}

// 此时使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改

public static void fun(Message< ? extends Fruit> temp) {

//temp.setMessage(new Banana()); //仍然无法修改!

//temp.setMessage(new Apple()); //仍然无法修改!

System.out.println(temp.getMessage());

}

}

此时无法在fun函数对temp进行添加元素,因为temp接收的是Fruit和他的子类,此时存储的元素应该是哪个子类无法确定。所以添加会报错!但是可以获取元素,如下:

public static void fun(Message< ? extends Fruit> temp) {

//temp.setMessage(new Banana()); //仍然无法修改!

//temp.setMessage(new Apple()); //仍然无法修改!

Fruit b = temp.getMessage();

System.out.println(b);

}

注意:通配符的上界,不能进行写入数据,只能进行读取数据

8.2.1 设定类型通配符的下限

🌸设定类型通配符时,除了可以设定类型通配符的上限,也可以对类型通配符的下限进行设定。设定类型通配符的下限后,类型实参只能是设定的类型或其父类型

设定类型通配符的下限的语法格式如下:

<?super 类>

案例一:

根据上述语法格式,对之前代码进行修改,演示如何设定类型通配符的下限,代码如下:

// 设定类型通配符的下限,此时传入的类型实参必须是 Number 类型 或者 其父类

public static void getElement(Collection< ? super Number> coll) {}

public static void main(String[] args) {

// 创建 Collection 对象,传入 Number 类型的类型实参

Collection<Number> l1 = new ArrayList<Number>();

// 创建 Collection 对象,传入 Object 类型的类型实参

Collection<Object> l2 = new ArrayList<Object>();

// 创建 Collection 对象,传入 Integer 类型的类型实参

Collection<Integer> l3 = new ArrayList<Integer>();

getElement(l1);

getElement(l2);

getElement(l3);

}

代码分析:

定义了方法 getElement(),设定类型通配符的下限为 Number,设定后调用该方法时传人的类型实参必须是 Number类型或其父类后面创建了3个 Collection 对象,分别传入了 Number 类型、Object 类型和 Integer 类型的类型实参然后分别以上面创建的3个 Collection 对象作为参数调用 getElement()方法,由于 l3 在创建时传入的 Integer类型 不是 Number 的父类,因此会出现编译时异常。

案例二:

<code>class Food {}

class Fruit extends Food {}

class Apple extends Fruit {}

class Plate<T> {

private T plate;

public T getPlate() {

return plate;

}

public void setPlate(T plate) {

this.plate = plate;

}

}

public class Demo {

public static void main(String[] args) {

Plate<Fruit> plate1 = new Plate<>();

plate1.setPlate(new Fruit());

fun(plate1);

Plate<Food> plate2 = new Plate<>();

plate2.setPlate(new Food());

fun(plate2);

}

public static void fun(Plate< ? super Fruit> temp) {

// 此时可以修改!!添加的是Fruit 或者Fruit的子类

temp.setPlate(new Apple());//这个是Fruit的子类

temp.setPlate(new Fruit());//这个是Fruit的本身

//Fruit fruit = temp.getPlate(); 不能接收,这里无法确定是哪个父类

System.out.println(temp.getPlate());//只能直接输出

}

}

注意:通配符的下界,不能进行读取数据,只能写入数据


小结 📖

泛型是将数据类型参数化,进行传递 使用 <T> 表示当前类是一个泛型类。 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换

使用泛型的好处

(1)提高类型的安全性

使用泛型后,将类型的检查从运行期提前到编译期。在编译期进行类型检查,可以更早、更容易地找出因为类型限制而导致的类型转换异常,从而提高程序的可靠性。

(2)消除强制类型转换

使用泛型后,程序会记住当前的类型形参,从而无须对传入的实参值进行强制类型转换,使得代码更加清晰和筒洁,可读性更高。

(3)提高代码复用性

使用泛型后,可以更好地将程序中通用的代码提取出来,在使用时传人不同类型的参数,避免了多次编写相同功能的代码,提高了代码的复用性。

(4)拥有更高的运行效率

使用泛型前,传入的实际参数值作为Object类型传递时,需要进行封箱和拆箱操作,会增加程序运行的开销;使用泛型后,类型形参中都需要使用引用数据类型,即传入的实际参数的类型都是对应的引用数据类型,避免了封箱和拆箱操作,减少了程序运行的开销,提高了程序的运行效率。

【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果我的这篇博客可以给你提供有益的参考和启示,可以三连支持一下 !!



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。