泛型

泛型类

定义形式 修饰符 class 类名 \< T,K,V… {} 其中T,K,V是自定义的随便写什么,但一般建议就写K、V、T等一个大写字母,毕竟本身含义就是泛型,没必要用特殊的标识符啥的定义。两边的空格数没有规定,连在一起或者分开都行 一般就与类名挨着,与左大括号间隔一个空格。泛型只在编译阶段起作用,运行阶段不起作用

定义与使用

package ljb;

/**
* @author 李家宝
* @date 2023/7/30 15:46
*/
public class Test<T> {
T name;

public T getName() {
return name;
}

public void setName(T name) {
this.name = name;
}

public static void main(String[] args) {

Test<String> test = new Test<>();
test.setName("李家宝");
System.out.println(test.getName());

}
}

类定义了泛型后,成员变量和成员方法就可以使用这个泛型了。在创建Test对象时,可以指定具体类型,上面的代码中就是指定T为String类型,那么Test类中的name也就是String类型了。Getter和Setter方法中的T也就是String类型了。但是如果没有指定,则T默认为Object类型

指定String类型

image-20230804005334196

未指定类型

image-20230804005244438

未指定具体类型时,Getter得到的也是Object类型对象,使用获取到的返回对象时就要进行强转。一般使用泛型类时是需要指定类型的,你不指定,就没有必要用泛型,我直接把类的成员变量和成员方法返回值全定义成Object类型不就是了。

泛型方法

定义形式 修饰符 返回类型 方法名(参数列表){}

定义与使用

package ljb;

/**
* @author 李家宝
* @date 2023/7/30 15:46
*/
public class Test<T> {
T name;

public T getName() {
return name;
}

public void setName(T name) {
this.name = name;
}

//这个T和类上面定义的不是同一个
public <T,K,V> T returnObj(T t,K k){
return t;
}

public static void main(String[] args) {

Test<Integer> test = new Test<>();
String ljb = test.returnObj("ljb",20);
test.setName(20);
System.out.println(ljb);
System.out.println(test.getName());
}
}

主要看上面的19-20行 首先要明白泛型方法和非泛型方法的区别,泛型方法有一个 这个相当于一个声明列表,和类上面那个一个意思。定义了这个泛型列表,形参中才能用这些类型。泛型的实际类型是根据传递过来的参数进行确定。方法就会根据调用时递的实际类型进行相关操作,但注意,是编译时期就会进行处理,并不是等到真正运行调用是才确定K,T,V它们的类型。

注意:

在上面的代码中,我故意把泛型方法returnObj的返回类型也写成了T,但这个T和类上那个T不是同一个T。就是说创建对象时如果指定泛型为String,并不意味着我的泛型方法返回值就是String。但是如果我的泛型方法泛型列表中没有定义T,那么这个T就是类上的T。(在代码中ctrl+左键点过去)

总结

泛型类是在类上声明一个泛型列表 就是 里面的个数至少一个。然后类中的成员变量就可以直接用这个泛型,而泛型的具体类型是创建对象时指定。

泛型方法是在方法上声明一个泛型列表 就是 里面的个数至少一个。然后参数列表中的变量就可以直接用这个泛型,而泛型的具体类型是传递参数时指定。

泛型接口

泛型接口和泛型类类似的。

有个要注意的点,泛型接口的实现类和实现接口时,如果没有声明泛型,实现接口的方法时,不管是泛型方法还是非泛型方法,泛型返回值会默认成Object类型,像下面这样 public class Test implements JK1

package ljb;

/**
* @author 李家宝
* @date 2023/7/30 15:46
*/
public class Test implements JK1{


@Override
public Object print1() {
return null;
}

@Override
public Object print() {
return null;
}
}


interface JK1<T> {

//这个T是接口上的T
T print1();

//这个T是泛型方法的T,与接口上的T没关系
<T> T print();
}

实现接口时如果指定了接口泛型的具体类型,那么实现的方法类型就是指定的类型,注意接口中的泛型方法的泛型不一定是指定的类型。泛型方法的注意那里讲了。

package ljb;

import com.sun.org.apache.bcel.internal.generic.NEW;

/**
* @author 李家宝
* @date 2023/7/30 15:46
*/
public class Test implements JK1<String>{

@Override
public String print(String s) {
return null;
}

@Override
public <T> T print1(T t) {
return null;
}


public static void main(String[] args) {
}
}


interface JK1<T> {

//这个T是接口上的T
T print(T t);

//这个T是泛型方法的T,与接口上的T没关系
<T> T print1(T t);
}

前面两种情况的代码,实现类并没有声明泛型列表,可以写也可以不写,随便写什么字母,并且与接口没关系。但如果实现接口时,还是将接口指定为泛型,那么实现类就必须要写泛型列表,并且列表中必须有一个与接口泛型相同的字母(泛型名)

package ljb;

/**
* @author 李家宝
* @date 2023/7/30 15:46
*/
public class Test<T,K,V> implements JK1<K>{


@Override
public K print1() {
return null;
}

@Override
public <T> T print() {
return null;
}
}


interface JK1<T> {

//这个T是接口上的T
T print1();

//这个T是泛型方法的T,与接口上的T没关系
<T> T print();
}

也就是说,在创建实现类对象时,确定接口泛型具体类型。

类型通配符 “ ?”

有一个需求,要求定义一个方法 要能处理以下两个种类型

/**
由于markdown识别不了尖括号<> 因此我写在代码块中
List<Integer>
List<Float>
**/

没学泛型之前,想到的办法可能就是进行重载,但是此处会有问题。

package ljb;

import java.util.ArrayList;
import java.util.List;
/**
* @author 李家宝
* @date 2023/7/30 15:46
*/
public class Test<T> {

// public static void fun1(List<Number> list) {
// //只能获取元素 返回Object类型 但无法对o进行强转 因为不知道List中的具体类型
// Number number = list.get(0);
// }

public static void fun1(List<Float> list) {
//只能获取元素 返回Object类型 但无法对o进行强转 因为不知道List中的具体类型
Float number = list.get(0);
}
public static void fun1(List<Integer> list) {
//只能获取元素 返回Object类型 但无法对o进行强转 因为不知道List中的具体类型
Integer number = list.get(0);
}

public static void main(String[] args) {
List<Integer> integers = new ArrayList<>();
List<Float> floats = new ArrayList<>();

fun1(integers);
fun1(floats);
}
}

可以看到,我对方法fun1进行了重载,我认为的是List < Integer>和List < Float> 是两个不同的类型,所以构成重载。但是实际上它俩的类型是一样的(类型擦除),代码会发生编译错误。那怎么办呢?可不可以试一下继承?Integer和Float都是Number的子类,我定义一个List< Number > 行不行?结果是并不行。(具体为啥我也不是很清楚,后期再补充吧。)

但是我们学了泛型啊,可以使用泛型来解决吗?答案是可以的

package ljb;

import java.util.ArrayList;
import java.util.List;
/**
* @author 李家宝
* @date 2023/7/30 15:46
*/
public class Test<T> {

public static <T> void fun1(List<T> list) {
//获取元素 返回实际类型
T t = list.get(0);
//打印
System.out.println(t);
}

public static void main(String[] args) {
List<Integer> integers = new ArrayList<>();
List<Float> floats = new ArrayList<>();
integers.add(20);
floats.add(20.0f);
fun1(integers);
fun1(floats);
}
}

但是有没有想过,但是我们在fun1方法中根本没办法通过引用变量t(13行)来调用对象的一些成员方法。就比如下面的代码

package ljb;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* @author 李家宝
* @date 2023/7/30 15:46
*/
public class Test<T> {

public static <T> void fun1(List<T> list) throws Exception {
//获取元素 返回实际类型
T t = list.get(0);
//我想获取对象t的名字
System.out.println(t.getName()); //这里编译报错
}

public static void main(String[] args) throws Exception {
List<Student> students =new ArrayList<>();
students.add(new Student("李家宝"));
fun1(students);
}
}
class Student{
public String name;

public Student(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

第17行会出现编译报错。那操作也太局限了,我总不能在fun1中把t强转成Student然后调用getName方法吧,因为它的实际类型是根据传过来的实参确定的,并不一定是Student类型啊! 怎么办? 其实这里可以利用反射机制进行操作。我们可以获取t的Class对象,然后反射它的成员方法并调用。代码如下

package ljb;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* @author 李家宝
* @date 2023/7/30 15:46
*/
public class Test<T> {

public static <T> void fun1(List<T> list) throws Exception {
//获取元素 返回实际类型
T t = list.get(0);
Class<?> clazz = t.getClass();
//反射成员方法getName
Method getName = clazz.getDeclaredMethod("getName");
//将对象t传入反射方法
System.out.println(getName.invoke(t));
}

public static void main(String[] args) throws Exception {
List<Student> students =new ArrayList<>();
students.add(new Student("李家宝"));
fun1(students);
}
}
class Student{
public String name;

public Student(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

反射的内容有一篇笔记有介绍,这里不再多说。但是使用反射显然有点麻烦,最终还是要靠我们的通配符”?”来解决。

通配符的使用

通配符 “ ? “ 是在为作为形参的泛型类指定泛型时使用。两者具体差别用代码展示

package ljb;

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

/**
* @author 李家宝
* @date 2023/7/30 15:46
*/
public class Test<T> {

//使用通配符? 通配符是实参类型 通配符就是用来接收所有类型对象
public static void fun1(List<?> list) {
//只能获取元素 返回Object类型 但无法对o进行强转 因为不知道List中的具体类型
Object o = list.get(0);
}

//使用泛型T 泛型是形参类型
public static <T> void fun2(List<T> list, T str) {
//可以返回Object 也可以返回T
Object object = list.get(0);
//可以强转,因为知道是类型T
T t2 = (T) object;

//也可以返回T 而通配符并不能返回'?'
T t = list.get(0);

//还能添加元素
list.add(str);

}

public static void main(String[] args) {
List<String> str = new ArrayList<>();
str.add("ljb");
fun1(str);
fun2(str, "nb");
}
}

在单独使用类型通配符或者泛型时,两者的区别不大。通配符刚开始那里有提出过一个需求,那个需求也能通过通配符解决。但是泛型可以支持添加元素,通配符不行。说白了,通配符就是能够接受所有类型。

说明一下上面的代码14所谓的不能强转,我的意思是,在fun1方法中,并不知道o的具体类型,所以无法强转为o的具体类型。但是不代表我不能在里面这样写 String str=(String) o; 因为我显然知道传进去的是String类型,这样写肯定没问题,能用运行成功。

并且对于fun2而言,我知道o就是T类型,所以也没有必要用Object object = list.get(0); 直接就用T t = list.get(0);就行

通配上下限

乍一看,感觉通配符还不如泛型好用啊?但是jdk为通配符提供了定义通配上限和下限的操作,让操作变得更灵活了。

通配上限
/**
List<? extends Aniaml> list //假设有这个关系 MinCat extends Cat extends Aniaml
当形参定义为上述类型时,可以传入List<Aniaml及Aniaml子类>,但是仍然无法进行添加操作,你想一下,方法中只能确定传过来的集合元素类型范围是Aniaml及其子类,我要如果我要添加一个Cat类型的元素,可能集合元素类型是MinCat,那么必然报错啊(你可以写MinCat minCat = (MinCat) new Cat(); 虽然编译不报错,但运行就报错了),就多态的知识。
**/
通配下限
/**
List<? super MinCat> list
当形参定义为上述类型时,可以传入List<MinCat及MinCat父类>,可以进行添加元素操作,但只能添加MinCat及MinCat子类类型的元素。你想一下,不管List的元素是什么类型,至少是MinCat或MinCat的父类,根据多态,子类可以转换成父类
**/

区别

  • 通配符更像是一个正则表达式符号,可以匹配所有的类型,并不能作为一种类型进行变量或方法返回值的声明。

  • 而泛型就一种真正的类型,可以声明为变量和方法返回值的类型。只不过它的实际类型是要根据传过来的参数实际类型进行确定。

类型擦除

看下面的代码,输出都是ArrayList类型型,输出结果为真。这与泛型是String还是Integer无关。

image-20230804081153365

image-20230804081051599

image-20230804081714497

image-20230804081832106

类型擦除在编译阶段完成

补充

时间:2023/8/4 16:00

学习红黑树时遇到下面这种写法

static class RBNode<K extends Comparable<K>,V> {

}

是一个静态内部类(不重要),整体的一个形式和我们之前定义泛型类时是一致的,就是声明一个泛型列表 < K, V,T … > 然后在类内部就能用这样字母声明变量和方法。只不过这里它给其中的泛型K做了一个约束,就是必须是Comparable接口的子类。

这里很奇怪,明明Comparable是一个接口,为什么用extends? 语法就是这样,没有为什么。这就是Java提供了一种语法来限制泛型的可实现类。它的语法规则就是下面这样

<T extends anyClass>   //这里的anyClass可以是接口,也可以是类。反正T必须实现或者继承anyClass

这可不要瞎用,不要与 通配符的上限写法搞混了

public void fun1(List<? extends Aniaml>);   //通配符这样用

public <T> void fun2(List<T extends Animal>); //下面这个不可以这样乱搞 错误!!!!!!

//仿照用在类上时的写法
public <T extends Animal> void fun2(List<T>); //传给fun2的list列表的的元素就必须继承Animal类,否则编译报错

时间:2023/8/4 19:00

静态方法中不能使用类定义时的泛型

image-20230804191539781

网上的解答:因为静态方法属于类的信息,而不是实例对象的信息。静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法就已经加载完成了(方法的参数类型,返回值类型啥的都需要提前确定好)。所以静态方法不能使用泛型

对于网上的解答还是没让我弄明白,泛型不是在编译期就被擦除了吗,不是都为Object类型了吗?那类上面为啥就能使用泛型了?类不是也得先加载吗?还有静态方法为什么又可以定义为泛型方法。对于这些可能还得再看看JVM的知识。