泛型
1. 介绍
泛型(Generics):是Java语言中一项重要特性,允许在类、接口和方法中使用参数化类型。目的是将具体类型参数化,使用时需要传入具体类型进行替换。通过泛型,可以编程通用、灵活的代码。提高代码的重用性和类型安全性。
参数分为实参与形参,而泛型属于形参。
public static void main(String[] args) { // 非泛型集合,存储元素用Object,add接收元素可以是基本数据类型、引用数据类型、布尔类型。 ArrayList<Object> arrayList = new ArrayList<>(); // 添加元素 arrayList.add("学Java的Bei"); arrayList.add(2024); for (int i = 0; i < arrayList.size(); i++) { // String o = (String) arrayList.get(i); // 强制String类型 // System.out.println(o); // 结果输出: 学Java的Bei int类型则 ClassCastException-转换异常 // 修改后: Object obj = arrayList.get(i); // 强制String类型 System.out.println(obj); // 结果输出: 学Java的Bei 2024 } }
ClassCastException异常:是JVM在检测到两个类型转换不兼容时引发的运行时异常。
// 非泛型集合 public static void main(String[] args) { // 非泛型集合,存储元素用Object类型,add接收的类型可以是基本数据类型,引用数据类型,布尔类型 ArrayList<Object> nonGenericCollectionList = new ArrayList<>(); // 列表内添加不同类型的对象 nonGenericCollectionList.add("学Java的Bei"); // 字符串 nonGenericCollectionList.add(20240225); // 整数 nonGenericCollectionList.add(true); // 布尔 // 列表内获取元素,并进行元素转换 String string = (String) nonGenericCollectionList.get(0); int num = (int)nonGenericCollectionList.get(1); Boolean bool = (boolean)nonGenericCollectionList.get(2); // 打印元素 System.out.println("string = " + string); System.out.println("num = " + num); System.out.println("bool = " + bool); // string = 学Java的Bei // num = 20240225 // bool = true }
使用非泛型集合时,需明确存储每个元素的数据类型,否则会引发 类型转换异常 ClassCastException。
// 泛型集合 // 需要一个存储字符串的列表,并且希望这个列表能够保证类型安全,那么我们可以使用泛型集合 ArrayList。 public static void main(String[] args) { // 创建一个存储字符串的泛型集合 ArrayList<String> stringList = new ArrayList<>(); // 向集合内添加元素 stringList.add("Hello,"); stringList.add("Generic."); // 遍历打印 System.out.println("字符串集合内的元素:"); for (String list : stringList) { System.out.println(list); } // 从集合内获取元素 String accessElement = stringList.get(0); System.out.println("第一个元素为:" + accessElement); }
// 泛型集合 // 创建一个泛型方法,该方法接受一个泛型列表作为参数,并打印出列表中的所有元素。这样的方法可以接受任何类型的列表,并且具有通用性。 public static void main(String[] args) { ArrayList<Integer> integerList = new ArrayList<>(); integerList.add(2024); ArrayList<String> stringList = new ArrayList<>(); stringList.add("Hello,2024!"); System.out.println("整数列表:"); printlnList(integerList); System.out.println("字符串列表:"); printlnList(stringList); } // 泛型方法,接受一个泛型列表并打印其中所有元素 public static <T> void printlnList(ArrayList<T> arrayList) { for (T element : arrayList) { System.out.println(element); } }
3. 泛型类
泛型类是使用泛型类型参数的类。允许类中某些字段、方法或构造函数接受特定类型的数据,而这些类型在类被实例化时才确定。
泛型标识(也称为类型参数):在Java中指定泛型类,接口或方法的参数类型。允许在编写代码时使用占位符来表示数据类型,而不需提前确定具体的参数类型;当使用时,再用确定的数据类型替换我们的标识。
在定义泛型类、接口或方法时,使用尖括号<T>、<E>或其他标识符来声明参数类型;这些标识符可以是任何合法的Java标识符,通常用单个大写字母表示,以表它们是参数类型。
- <T>:通用泛型类型,通常表示任意类型;
- <E>:集合元素泛型类型,通常表示集合中的元素类型;如List、Set
- <K,V>:映射键,值:表示键值对中键和值的参数类型;如Map
泛型类在创建对象时,如果没有指定类型,按照Object类型操作。
// 泛型标识 public class GenericIdentifier<T> { // <T> 是一个泛型标识,它表示任意类型。 // 在实例化 Box 类时,指定了具体的数据类型(整数和字符串),这样就替换了泛型标识,使得 Box 类可以存储不同类型的数据。 // 下面的T仅仅表示的是一种参数类型,这个参数类型是一个变量,可以指代任意一种引用数据类型。 // T可以换成 A-Z 之间的任何一个字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替,在可读性上可能会弱一些。 private T date; public GenericIdentifier(T date) { this.date = date; } public T getDate() { return date; } public void setDate(T date) { this.date = date; } } class Main { public static void main(String[] args) { // 创建一个存储整数的GenericIdentifier实例 GenericIdentifier<Integer> integerGenericIdentifier = new GenericIdentifier<>(10); // 创建一个存储字符串GenericIdentifier实例 GenericIdentifier<String> stringGenericIdentifier = new GenericIdentifier<>("学Java的Bei"); // 获取并打印数据 System.out.println("整数:" + integerGenericIdentifier.getDate()); // 整数:10 System.out.println("字符串:" + stringGenericIdentifier.getDate()); // 字符串:学Java的Bei } }
注意:我们 new 出的对象都是 来自 构造方法(有参构造与无参构造)。
4. 从泛型类派生子类
在Java可以从泛型类派生子类,但要注意一些限制和约束。
1) 如果子类也是泛型类,并且使用父类的参数类型,那么子类的参数类型标识应与父类的参数类型标识一致。否则,在子类中无法得到具体的数据类型,编译器会报错。
// 从泛型类派生子类 public class NoteOne<T> { // SubClass<T> 是 NoteOne<T> 的子类,并且也是一个泛型类。 // 子类 SubClass 继承了父类 NoteOne 的类型参数 T,使得子类可以使用相同的类型参数。 // 这样,子类可以保留父类的泛型类型,并且可以使用相同的类型参数来实例化对象。 private T data; public NoteOne(T data) { this.data = data; } public T getData() { return data; } } // 子类也是泛型类,且继承父类的参数类型 class SubClass<T> extends NoteOne<T>{ public SubClass(T date) { super(date); // super(data) 表示调用父类的构造方法,并将参数 data 传递给父类的构造方法进行处理。 } } class Main { public static void main(String[] args) { // 创建一个存储整数的SubClass实例 SubClass<Integer> integerSubClass = new SubClass<>(2024); System.out.println("整数:" + integerSubClass.getData()); // 创建一个存储字符串的SubClass实例 SubClass<String> stringSubClass = new SubClass<>("学java的Bei"); System.out.println("字符串:" + integerSubClass.getData()); } }
public class NoteTwo<T> { // 子类 SubBox 继承了父类 NoteTwo<T>,但子类自身不是泛型类。 // 在子类的构造方法中,通过 super(data) 指定了父类 NoteTwo 的泛型数据类型为 Integer。 // 这样,子类 SubBox 就可以正确地实例化父类 NoteTwo<Integer>。 private T data; public NoteTwo(T data) { this.data = data; } public T getData() { return data; } } // 子类不是泛类型 class SubClassTwo extends NoteTwo<Integer> { public SubClassTwo(Integer date) { super(date); // 指定父类的泛型数据类型为Integer } } class MainTwo{ public static void main(String[] args) { // 创建子类实例,需明确指定父类的泛型数据类型为Integer SubClassTwo subClassTwo = new SubClassTwo(20240225); // 调用父类方法 System.out.println("参数:" + subClassTwo.getData()); // 参数:20240225 } }
在上面super 点的父类 是 父类的 构造方法。
3) 当父类是泛型类而子类不是泛型类时,子类继承父类后,父类的泛型类型参数会被参数,编译器会视为Object类型。
// 父类泛型 public class NoteThree<T> { private T data; public NoteThree(T data) { this.data = data; } public T getData() { return data; } } // 子类泛型 class SubClassThree extends NoteThree<String>{ public SubClassThree(String date) { super(date); // 指定父类的泛型类型为String } } class MainThree{ public static void main(String[] args) { // 创建子类实例,需明确指定父类泛型数据类型为String SubClass subClass = new SubClass("学Java的Bei"); // 调用父类方法 System.out.println("结果:" + subClass.getData()); // 结果:学Java的Bei } }
4) 子类的泛型标识可以添加多个,但必须有一个和父类的泛型标识一样。子类的泛型标识数量不一样要与父类相同。
// 可以正确地继承父类的行为,且还可以添加自己的特定行为。 public class NoteFour<T> { private T data; public NoteFour(T data) { this.data = data; } public T getData() { return data; } } // 子类拥有两个泛型类型,但必须有一个与父类相同 class SubClassFour<T,V> extends NoteFour<T>{ private V info; public SubClassFour(T data, V info) { super(data); this.info = info; } public V getInfo() { return info; } } class MainFour{ public static void main(String[] args) { SubClassFour<Integer, String> subClassFour = new SubClassFour<>(20240224,"学Java的Bei"); System.out.println("info--" + subClassFour.getInfo()); System.out.println("data--" + subClassFour.getData()); } }
5) 若子类拥有多个泛型标识,创建子类对象时需对这些泛型标识制定具体的类型。为了确保类型的一致性,并遵循泛型类的规范。
public class NoteFive<T> { private T data; public NoteFive(T data) { this.data = data; } public T getData() { return data; } } // 子类拥有两个泛型类型,但必须有一个与父类相同 class SubClassFive<T,U> extends NoteFive<T> { private U info; public SubClassFive(T data, U info) { super(data); this.info = info; } public U getInfo() { return info; } } class MainFive{ public static void main(String[] args) { // 创建SubClassFive对象,需指定 T,U的具体类型 SubClassFive<String, Integer> subClassFive = new SubClassFive<>("学Java的Bei:龙年大吉!", 20240224); System.out.println("info--" + subClassFive.getInfo()); System.out.println("data--" + subClassFive.getData()); } }
总结:
- 泛型是定义的时候用,但真正使用的时候,必须确定类型;
- 定义的时候是泛型类,使用的时候没有指定,则就是Object;
- 如果父类是泛型类,子类不是泛型类,继承父类后,父类没有声明,则默认是Object;
- 子父关系中,定义的时候,子类的泛型类必须要和父类的泛型类一致,否则会报错;
- 如果子类不是泛型类,父类要明确泛型的数据类型;
- 子类的泛型标识可以添加多个,但必须有一个和父类的泛型标识一致;
5. 泛型接口
1) 泛型接口的定义语法
泛型接口的定义语法与普通接口的语法相似。在接口名称除使用尖括号<>声明一个或多个泛型参数。这些泛型参数可在接口中的方法中作为返回值类型或参数类型使用,从而使得接口更具有泛型特特征。
// 泛型接口定义语法 // DefineSyntax是一个泛型接口,它有一个类型参数 T。 // 接口中的方法 getValue() 返回的类型和 setValue() 方法的参数类型都是泛型类型参数 T。 // 这样,实现这个接口的类就可以根据需要指定具体的类型参数,或者保留泛型参数。 public interface DefineSyntax<T> { T getValue(); void setValue(T value); }
2) 泛型接口的使用
- 如果实现了不是泛型类,那么实现的接口的泛型接口必须明确具体的数据类型。
实现接口类指定泛型接口类型为String,那么接口类内的泛型标识符T就是String类型,返回值也是String类型。
public interface DefineSyntax<T> { T getValue(); void setValue(T value); } // 指定泛型T的参数类型为String class ImplClass implements DefineSyntax<String> { private String value; @Override public String getValue() { return value; } @Override public void setValue(String value) { this.value = value; } }
- 如果实现类是泛型类且实现了泛型接口,实现类和接口的泛型类型必保持一致,否则无法接受具体的数据类型。
public interface TypeConsistency<T> { T getValue(); void setValue(T value); } // 泛型类型要与实现类的泛型类型一致 class TypeClass<T> implements TypeConsistency<T>{ private T value; @Override public T getValue() { return value; } @Override public void setValue(T value) { this.value = value; } }
6. 泛型方法
泛型类和泛型方法的区别:
泛型类:是在实例化类的时候指明泛型的具体类型;
泛型方法:是在调用方法的时候指明泛型的具体类型;
1) 定义语法
- public与返回之类中间的<T>非常重要,可以声明此方法为泛型方法;
- 只有在泛型列表中声明了泛型标识的方法才是泛型方法;
- <T>表明该方法将使用泛型类型T;
- 与泛型类的定义一样,此处T可写为任意标识;
泛型方法:就是将方法参数类型中的泛型,提前在前面声明,必须用<>包起来。
格式:修饰符 <泛型标识> 返回值类型 方法名(参数列表) { // 方法体 }
// 定义语法 public class DefinitionSyntax { // isEqual 方法是一个泛型方法,它接受两个参数,并比较它们是否相等。 public static <T> boolean isEqual(T first, T second) { // 返回 true/false return first.equals(second); } } // 使用 class Main{ public static void main(String[] args) { // 使用 isEqual方法比较两个整数是否相等 int num1 = 2024; int num2 = 0226; boolean result = DefinitionSyntax.isEqual(num1, num2); System.out.println("结果:" + result); // 比较:结果:false // 使用 isEqual 方法比较两个字符串是否相等 String str1 = "学Java的Bei"; String str2 = "学Java的Bei"; boolean equal = DefinitionSyntax.isEqual(str2, str1); System.out.println("结果:" + equal); // 比较:结果:true } }
2) 静态泛型方法
静态泛型方法:是指在类中声明的静态方法,该方法可以使用泛型类型参数。
静态泛型方法与普通泛型方法的区别是:静态泛型方法是在静态方法中声明的,而普通方法可以是静态的或非静态的。
// 静态泛型方法 public class StaticGenericMethod { // 静态类型方法:比较两个元素是否相等 public static <T> boolean isEqual(T first, T second) { // 返回 true/false return first.equals(second); } // 静态类型方法:打印数组内所有的元素 public static <E> void printArray(E[] array) { for (E element : array) { System.out.println("打印--" + element); } System.out.println(); } } // 使用这两个静态泛型方法进行比较和打印数组元素。 class StaticMain{ public static void main(String[] args) { // 使用 静态泛型方法 isEqual方法比较两个整数是否相等 int num1 = 2024; int num2 = 0226; boolean result = StaticGenericMethod.isEqual(num1, num2); System.out.println("结果:" + result); // 比较:结果:false // 使用 静态泛型方法 isEqual 方法比较两个字符串是否相等 String str1 = "学Java的Bei"; String str2 = "学Java的Bei"; boolean equal = StaticGenericMethod.isEqual(str2, str1); System.out.println("结果:" + equal); // 比较:结果:true // 使用 静态泛型方法 打印 printArray 方法打印元素数组 Integer[] intArray = {1, 3, 5, 6, 7, 2024}; StaticGenericMethod.printArray(intArray); // 打印--1 ... String[] strArray = {"学Java的Bei", "在看的你-", "胜过彭于晏"}; StaticGenericMethod.printArray(strArray); // 打印--学Java的Bei,打印--在看的你- ... } }
3) 泛型可变参数
泛型可变参数是Java内一种灵活的参数类型,允许方法接受可变数量的参数,且这些参数可以是任意类型的泛型参数。
可变参数的定义为在形参泛型标识后加 ... ,使得方法可以接受不定数量的参数。... 背后的本质为 T[]数组。
// 泛型可变参数 public class GenericVarargs { // 泛型可变参数方法:打印任意数量的元素 public static <T> void printElements(T... elements) { for (T element : elements) { System.out.println("打印:" + element); } System.out.println(); } } // 使用 class VarargsMain { public static void main(String[] args) { // 使用泛型可变参数打印不同类型的元素 GenericVarargs.printElements(2024,0226,20.28); GenericVarargs.printElements("学Java的Bei", "在看的你", "胜过彭于晏"); } }
7. 泛型通配符
泛型通配符是Java内用于表示未知泛型类型的符号,用 ? 表示。作用是增加泛型类型的灵活性,允许在不确定使用泛型类型时使用泛型。
注意: ? 是代替的具体的类型实参,而非类型形参。此时只能接受数据,不能往该集合中存储数据。不能使用泛型标识A-Z,因为泛型标识代表形参,这里需要实参。
public class Test001<E> { // 声明成员变量 private E first; public E getFirst() { return first; } public void setFirst(E first){ this.first = first; } }
public class Test002 { public static void show(Test001<Integer> test) { Integer first = test.getFirst(); System.out.println(first); } public static void main(String[] args) { // 调用 Test001<Integer> test001 = new Test001<>(); // 赋值 test001.setFirst(2024); show(test001); Test001<String> test0011 = new Test001<>(); test0011.setFirst("学Java的Bei"); // show(test0011); // 已经被上面定义为了Integer类型,这里的String类型会报错 // 使用通配符 ? 后 Test001<String> test0012 = new Test001<>(); test0012.setFirst("学Java的Bei"); show1(test0012); // 结果:学Java的Bei Test001<Object> test0013 = new Test001<>(); test0013.setFirst(2024); show1(test0013); // 结果:2024 } public static void show1(Test001<?>test001) { Object first = test001.getFirst(); System.out.println(first); } }
通配符可以在泛型类、泛型方法、泛型接口使用,主要三种形式:
1) 上限通配符
? extends T:表示通配符可以匹配 T类型 及其 T类型的子类;通配符表示的是一个上界,表示类型的上界是 T 类型或 T 的子类;
简单说,? extends T :这种通配符表示一个范围,即表示匹配的类型为T 或 T的某个子类,在泛型中被称为上届通配符。就是取出某种类型或者某种类型的子类型。
定义语法:类/接口<? extends 实参类型>
举例:
// 设有一个类层次结构,有一个父类 UpperLimit,以及两个子类 ClassOne 和 ClassTwo。 // 现有一个泛型容器 List<? extends UpperLimit>,它可以匹配 UpperLimit 类型以及 UpperLimit 的子类,比如 ClassOne 和 ClassTwo。 class UpperLimit {} class ClassOne extends UpperLimit{}; class ClassTwo extends UpperLimit{}; class MainUpper{ public static void main(String[] args) { List<? extends UpperLimit> upperLimits; upperLimits = new ArrayList<ClassOne>(); // 合法 upperLimits = new ArrayList<ClassTwo>(); // 合法 } }
2) 下限通配符
? super T:在Java泛型中表示泛型类型的下界。作用为限制通配符所代表的类的下限T或T的父类。意味着可以使用下限通配符的方法传递类型T或T的父类的对象。
简单来说:指定这个类型或这个类型的父类或者这个类型的父类的父类。
在 Java 中,使用下限通配符的集合只能接受指定下限类型或其子类型的对象,而不能接受其父类型的对象。
class Floor {} class ClassThree extends Floor{} class ClassFour extends Floor{} class MainFloor{ public static void main(String[] args) { List<? super ClassThree> floors = new ArrayList<>(); addFloor(floors); // 合法调用。因为 floors 列表可以接受 ClassThree 类型或者 ClassThree 的父类的对象. floors.add(new ClassThree()); // 合法操作 // floors.add(new Floor()); // 非法操作 // floors.add(new ClassFour()); // 非法操作 } // 设有一个方法 addFloor,它接受一个 List<? super ClassThree>,表示这个列表可以接受 ClassThree 类型或者 ClassThree 的父类的对象. public static void addFloor(List<? super ClassThree> floors){} }
8. 泛型擦除
当我们谈论泛型擦除时,我们实际上在讨论Java泛型的一个重要特性。Java泛型是在Java 5中引入的,它通过类型擦除来实现泛型化。
泛型擦除是Java泛型的一个重要特性,它指的是在编译时期,泛型类型信息被擦除,并转换为原始类型,使得在运行时期无法获取泛型的具体类型信息。尽管编写泛型代码可以提供类型安全性检查,但在运行时,泛型类型的行为与普通类型类似,无法区分不同类型的泛型实例。
重点强调:
-
运行时类型信息的丢失:由于类型擦除,泛型的运行时类型信息被丢失。这意味着你不能在运行时获得泛型的具体类型参数信息。
-
泛型实例的Class对象:不同泛型实例的Class对象在运行时是相同的。例如,
List<String>
和List<Integer>
的实例在运行时的Class对象都是List.class
。 -
泛型转译(Type Erasure):在编译期间,编译器会插入必要的转型代码来保证类型安全性。这些转型代码确保了泛型代码的类型约束。
1) 无限制泛型擦除
无限制擦除是指在Java泛型中,如果没有指定具体的类型参数,例如使用泛型类型而没有指定类型参数,那么在编译时会将泛型类型擦除为原始类型。这样的情况下,编译器无法对泛型的类型进行任何检查,因此会发出警告。
// 创建一个没有指定类型参数的泛型 List。这种情况下,在编译时会发出警告,因为缺乏类型参数,编译器无法对泛型进行类型检查. public class GenericErase { public static void main(String[] args) { List list = new ArrayList(); // 没有指定类型参数的泛型 list.add("学Java的Bei"); list.add(2024); for (Object obj : list) { // 在使用时,需要进行显示的类型转换,并且容易引发类型转换异常 String str = (String) obj; // 这里会引发 引发类型转换异常(ClassCastException) System.out.println(str); } } }
2) 指定了上限,上限是Number,泛型标识T在做泛型擦除的时候转换成了上限类型。
定义的时候是泛型标识E,在使用的时候给泛型标识定为Integer类型,编译结束后,进行泛型擦除,生成了class字节码文件,通过反射,此时成员变量key的数据类型就成了Number类型,包括浮点数、整数等。
public class Test01<E extends Number> { private E key; private Integer num; private String str; public E getKey(){ return key; } public void setKey(E key) { this.key = key; } public Integer getNum() { return num; } public void setNum(Integer num) { this.num = num; } public String getStr() { return str; } public void setStr(String str) { this.str = str; } }
public static void main(String[] args) { // 创建类对象 Test01<Integer> test01 = new Test01<>(); // 反射获取对象的字节码文件 Class<? extends Test01> aClass = test01.getClass(); // 反射获取所有成员变量 Field[] declaredFields = aClass.getDeclaredFields(); // 对成员变量进行遍历 for (Field field : declaredFields) { // 打印成员变量的名称和类型 System.out.println(field.getName() + ":" + field.getType().getSimpleName()); // 结果:key:Number; num:Integer; str:String } }
3) 擦除方法中类型定义的参数
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)