Java 泛型

从 Java 5以后,Java 引入了“参数化类型(parameterized type)”的概念,允许程序在创建集合时,指定集合元素的类型,例如List<String>,这表明该 List 只能保存字符串类型的对象。Java 的参数化类型被称为 泛型(Generic)

使用泛型#

通过在泛型类型后增加一对尖括号,尖括号中放入一个类型,例如

Copy
List<String> list = new ArrayList<String>(); Map<String,Integer> map = new HashMap<String,Integer>();

从 java 7 开始,Java 允许在构造器后不需要带完整的泛型类型,只需要给出一对尖括号<> 即可,可以通过定义变量推断尖括号里应该是什么泛型类型

Copy
List<String> list = new ArrayList<>(); Map<String,Integer> map = new HashMap<>();

集合声明泛型后,则只能存放该泛型类型的元素

Copy
List<String> list = new ArrayList<>(); Map<String,Integer> map = new HashMap<>(); list.add("Java"); list.add(new Object()); // 报错,参数不匹配 map.put("Java",60); map.put("C#",new Object()); // 报错,参数不匹配

深入泛型#

编写泛型类#

所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参(泛型)将在声明变量、创建对象、调用方法时动态地指定

Copy
public class Apple<T> { // 使用T类型定义变量 private T info; public Apple() { } public Apple(T info) { this.info = info; } public void setInfo(T info) { this.info = info; } public T getInfo(){ return this.info; } }
Copy
public class GenericTest { public static void main(String[] args) { // 由于传给T形参地类型是String,所以构造器参数只能是String Apple<String> a1 = new Apple<>("红苹果"); // 由于传给T形参地类型是Double,所以构造器参数只能是Double 或 double Apple<Double> a2 = new Apple<>(20.5); System.out.println("a1:" + a1.getInfo()); // 通过 该方法参数只能是String a1.setInfo("青苹果"); System.out.println("a1:" + a1.getInfo()); System.out.println("a2:" + a2.getInfo()); } }

输出

Copy
a1:红苹果 a1:青苹果 a220.5

从泛型派生子类#

当创建了带泛型声明地接口、父类之后,可以为该接口创建实现类,或从该父类派生子类

子类或实现类不能再包含泛型类形参

Copy
// 定义类A 继承 Apple类,Apple 类不能跟泛型形参 public class A extends Apple<T> {}

但是可将子类的泛型形参传入给父类

Copy
public class A<T> extends Apple<T> {}

同理也可以

Copy
public class A extends Apple<String> {}
Copy
public class A extends Apple<String> { // 重新父类方法,因为传入给父类形参是String类型 所以返回值也要是String @Override public String getInfo() { return "子类" + super.getInfo(); } public static void main(String[] args) { A a = new A(); a.setInfo("Java"); System.out.println(a.getInfo()); } }

输出

Copy
子类Java

注意,泛型类例如ArrayList<String> 并不是ArrayList的子类,并不会生成class文件,instanceof 运算符后也不能使用泛型

类型通配符#

类型通配符是一个问号(?),将一个问好作为类型实参传给List 集合,写作:List<?>,这个问好(?)被被称为通配符,它的元素类型可以匹配任何类型

Copy
public class GenericTest { // 不论使用任何类型的List调用此方法,程序都可以访问集合中的元素,其类型是Object public static void test(List<?> list) { for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } } public static void main(String[] args) { List<String> list1 = new ArrayList<>(); list1.add("Java"); list1.add("C#"); list1.add("PHP"); test(list1); List<Double> list2 = new ArrayList<>(); list2.add(20.5); list2.add(21.2); test(list2); } }

由于无法确定集合中元素的类型,这种带通配符的List 仅仅表示它是各种泛型List 的父类,并不能把元素加入其中。

Copy
List<?> list = new ArrayList<>(); // 下面程序将会引起编译错误 list.add("ad");

设定类型通配符的上限#

有时候,我们希望泛型类型只代表某一类泛型类型的父类,我们可以通过<? extends 类名> 指定泛型类型的上限

Copy
public abstract class Shape { public abstract void draw(Canvas c); }
Copy
public class Circle extends Shape { @Override public void draw(Canvas c) { System.out.println("在画布" + c + "上画了个圆"); } }
Copy
public class Rectangle extends Shape{ @Override public void draw(Canvas c) { System.out.println("在画布" + c + "上画了个矩形"); } }
Copy
public class Canvas { public void drawAll(List<? extends Shape> shapes){ for(Shape shape : shapes){ shape.draw(this); } } }
Copy
public class GenericExtendsTest { public static void main(String[] args) { List<Circle> circles = new ArrayList<>(); List<Rectangle> rectangles = new ArrayList<>(); Canvas canvas = new Canvas(); canvas.drawAll(circles); canvas.drawAll(rectangles); List<String> s = new ArrayList<>(); // 引发编译错误,因为String不属于Shape 或 其子类 canvas.drawAll(s); } }

对于更广泛的泛型来说,指定通配符上限就是为了支持类型型变。比如 XY 的子类,这样I<X> 就相当于I<? extends Y> 的子类,可以将I<X> 赋值给A<? extends Y> 类型的变量,这种型变方式被称为协变

对于协变的泛型而言,它只能调用泛型类型作为返回值类型的方法;而不能调用泛型类型作为参数的方法。口诀:协变只出不进

设定类型通配符的下限#

除了可以指定通配符的上限以外,也允许指定通配符的下限,通过用<? super 类型> 的方式来指定,与上限相反,比如 XY 的子类,程序可以将I<Y> 赋值给I<? super X> 类型的变量,这种型变方式被称为逆变

对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种你变得泛型集合只能向其中添加元素。 口诀:逆变只进不出

案例:实现将src 集合 复制到 dest 集合,因为 dest 集合可以保存src 集合中的元素,因此 dest 集合的元素类型 是 src 的父类

Copy
public class MyUtils { /** * 实现将`src` 集合 复制到 `dest` 集合 * @param dest T类型为下限的集合 * @param src T类型集合 * @return 最后添加的元素 * @param <T> */ public static <T> T copy(Collection<? super T> dest, Collection<T> src) { T last = null; for (T ele : src) { last = ele; dest.add(ele); } return last; } public static void main(String[] args) { List<Number> numbers = new ArrayList<>(); List<Integer> integers = new ArrayList<>(); integers.add(5); Integer last = copy(numbers, integers); System.out.println("last:" + last); System.out.println("numbers:" + numbers); } }

输出

Copy
last:5 numbers:[5]

设定泛型形参的上限#

Java 泛型不仅允许在使用通配符形参时设定上限,并且可以在定义泛型形参时设定上限

例如

Copy
public class Apple<T extends Number> {

也可以设置多个

Copy
// T类型必须是Number 类或其子类,并且实现Serializable接口 public class Apple<T extends Number & java.io.Serializable> {

泛型方法#

当定义类、接口时没有使用泛型形参,但定义方法时想自己定义泛型形参,我们可以通过泛型方法来实现

定义泛型方法#

语法格式如下

Copy
修饰符 <T, S> 返回值类型 方法名(形参列表) { // 方法体... }

上面设定通配符下限的案例就是泛型方法

Copy
public static <T> T copy(Collection<? super T> dest, Collection<T> src) { T last = null; for (T ele : src) { last = ele; dest.add(ele); } return last; }

同样也可以设定泛型形参的上限和设定类型通配符的上限,例如 Java 的 Collection 接口中有两个方法定义

Copy
boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c);

可以改为

Copy
<T> boolean containsAll(Collection<T> c); <T extends E> boolean addAll(Collection<T> c);

更改上述Canvas

原类

Copy
public class Canvas { public void drawAll(List<? extends Shape> shapes) { for (Shape shape : shapes) { shape.draw(this); } } }

改为

Copy
public class Canvas { public <T extends Shape> void drawAll(List<T> shapes) { for (Shape shape : shapes) { shape.draw(this); } } }

泛型构造器#

如泛型方法一样,构造器也可以声明泛型形参

Copy
public class Foo { public <T> Foo(T t) { System.out.println(t); } public static void main(String[] args) { new Foo("Java"); new Foo(200); // 指定泛型类型为String new <String>Foo("C#"); // 因为指定了泛型类型为String类型,所以下行代码传入数值类型会报错 new <String>Foo(20); } }

如果是泛型类,并且显示指定了泛型构造器的泛型类型时,则泛型类后面不可以再省略泛型类型,如下

Copy
public class MyClass<E> { public <T> MyClass(T t) { System.out.println(t); } public static void main(String[] args) { // E形参String类型,T形参Integer类型 // 不显式指定泛型构造器的泛型形参类型时,后面可以用<>省略 MyClass<String> myClass1 = new MyClass<>(20); // 显式指定泛型构造器的泛型形参类型时,后面<>中也要显示指定类的泛型形参类型 MyClass<String> myClass2 = new <Integer>MyClass<String>(20); // 显式指定泛型构造器的泛型形参类型时,MyClass后面不可以用<>省略,下方代码报错 MyClass<String> myClass4 = new <Integer>MyClass<>(20); } }

擦除和转换#

定义变量时,如果没有为泛型类指定实际的类型,此时被称为 raw type(原始类型) ,默认声明该泛型形参指定的是第一个上限类型。例如把一个List<String> 类型的对象赋值给List,则该List 对集合元素的类型检查变成了泛型参数的上限(即Object

Copy
public class Cat<T extends Number> { T weight; public Cat(T weight) { this.weight = weight; } public T getWeight() { return weight; } public void setWeight(T weight) { this.weight = weight; } }
Copy
public class ErasureTest { public static void main(String[] args) { // cat 泛型形参为Integer类型 Cat<Integer> cat = new Cat<>(9); // c的泛型形参类型为Number类型 Cat c = cat; Integer weight_int = cat.getWeight(); Number weight_num = c.getWeight(); // 下方代码报错,因为c的泛型形参是Number类型 weight_int= c.getWeight(); } }

从逻辑上来看,如果直接把List 转换为List<String> 对象应该引起编译错误,但实际上不会。对泛型而言,可以直接把一个List 对象赋给一个List<String> 对象

Copy
public class ErasureTest2 { public static void main(String[] args) { List list = new ArrayList(); list.add(20); list.add(1); list.add(21); List<String> stringList = list; System.out.println(stringList.get(0)); } }

运行输出

Copy
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String at GenericDemo.ErasureTest2.main(ErasureTest2.java:13)

虽然程序编译没问题,但运行时当集合试当将元素当String取出来时还是会报错

posted @   赵小源  阅读(67)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示
CONTENTS