1. Java泛型概述
1.1 Object实现参数任意化
-
考虑自己实现一个容器类,可以支持对不同类型数据的增删改查。
-
JDK 1.5之前,由于不支持泛型,所以需要使用
Object
创建数组作为容器。获取item时,需要显式地进行强制类型转换。class MyList { public static final int LENGTH = 10; private Object[] list = new Object[LENGTH]; private int size = 0; public void add(Object object) { list[size++] = object; } public Object get(int index) { return list[index]; } public int size() { return size; } } // 使用方式 String str = (String) list.get(1);
-
针对上述代码,java编译时不会检查非法的强制类型转换,在运行时才会报错。
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String at ObjectGeneric.main(ObjectGeneric.java:8)
-
总结:
- 通过使用
Object
来实现参数的任意化,带来的缺点是需要进行显式地强制类型转换。 - 针对类型转换错误的情况,编译器不会提示错误,在运行时才会抛出
ClassCastException
。 - 因此,使用
Object
实现参数的任意化存在安全隐患。
1.2 泛型的引入
- 在JDK源码中,我们经常看到类似代码,他们使用大写字母去限定自己的参数类型。这就是泛型!
public interface List<E> extends Collection<E>
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
-
泛型的意思就是泛指的类型(参数化类型),即所操作的数据类型可以以参数的形式进行指定。
class Printer<T>{ private T item; public Printer(T item){ this.item=item; } public void print(){ System.out.println(item.getClass().getName()); } }
-
Java 泛型(generics)是 JDK 5 中引入的一个新特性,:
- 泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型转换,提升代码的健壮性。
- 泛型会进行隐式地、自动的强制类型转换,可以提高代码的利用率。
- 参考文档链接:
Java学习之泛型及应用场景
2. 泛型的类型
2.1 泛型类
- 泛型类型的语法:
class ClassName<泛型通配符identify>{
// 泛型通配符可以是A-Z的任意大写字母以及'?'
private identify item; // 使用泛型通配符定义成员变量
public identify getItem(){
// 返回泛型值的方法,并非是泛型方法
return item;
}
...
}
-
泛型类在Java的容器(
Collection
和Map
)中最常见,通过泛型实现了数据类型的任意化,灵活、安全、易维护。 -
泛型类的简单实例:
class MyGeneric<T> { private T item; public void setItem(T item) { this.item = item; } public T getItem() { return item; } } // 使用泛型类 public static void main(String[] args) { // 创建泛型类对象时不指定类型,使用时与Object定义的类一样,需要进行显式转换 MyGeneric generic1 = new MyGeneric(); generic1.setItem(new Double(12.5)); Double number = (Double) generic1.getItem(); System.out.println("number: " + number); // 创建泛型类对象是指定类型,可以充分利用泛型的优势 MyGeneric<Integer> generic2=new MyGeneric<>(); // generic2.setItem(12.3); // 编译无法通过,提示double类型无法转换成Integer类型 generic2.setItem(12); if (generic2.getItem() instanceof Integer){ System.out.println("Item is Integer"); } }
2.2 泛型方法
-
泛型方法: 在权限修饰符和返回值之间增加了泛型通配符,就让普通方法成为了泛型方法。
// 有参数的泛型方法 public <E> void printInfo(E input){ System.out.println(input); } // 调用泛型方法 obj.printInfo("sunrise"); // 无参数的泛型方法 public <E> List<E> createList(){ return new ArrayList<>(); } // 调用泛型方法 List<String> list = obj.createList(); // 创建的是String类型的list
-
注意:
- 泛型方法与泛型类是相互独立的:
① 泛型类使用T
作为通配符,泛型方法也使用T
。实际使用时,泛型类传入类型为String
,而泛型方法可以为任意类型,不受泛型类的限制。
② 泛型方法可以在泛型类中定义,也可以在普通类中定义;泛型类不一定包含泛型方法。 - 泛型类中,使用了泛型成员的方法并非泛型方法,泛型方法一定要有单独的声明。
- 泛型方法与可变参数
-
Java方法中,通常使用
...
来定义可变长度的参数。public void printNames(String... names) { for (String name : names) { System.out.print(name + " "); } } // 传入任意数量的参数 generic.printNames("lucy", "grace", "john", "张三");
-
泛型参数会被编译器转型为一个数组,因此可以当做普通的数组进行遍历。
public void printNames(String... names) { for (int i=0;i<names.length;i++){ System.out.print(names[i] + " "); } }
-
可变参数规定了传入的参数类型必需是一致的,可以将泛型方法与可变参数结合,传入不同类型的参数。
public <T> void printNumbers(T... args) { for (T arg : args) { System.out.print(arg + " "); } } // 方法的调用 generic.printNumbers("12.3", 24, 24.5, 2.4f);
2.3 泛型接口
-
泛型接口与泛型的定义基本一致
//定义一个泛型接口 public interface Generator<T> { public T next(); }
-
如何实现泛型接口?
-
泛型类在实现泛型接口时,未向泛型接口传入泛型实参。则在定义泛型类时,需要为该类加入泛型声明。
class GenericClass2<T> implements GenericInterface<T>{ @Override public T next() { return null; } }
-
泛型类在实现泛型接口时,向泛型接口传入了泛型实参。则在定义泛型类时是,无需为该类加入泛型声明。
class GenericClass1 implements GenericInterface<String>{ @Override public String next() { return null; } }
3. 泛型通配符
3.1 泛型通配符概述
-
泛型通配符可以使用
A-Z
的任意一个字符,不影响泛型的效果,但会影响理解。class GenericClass2<A> implements GenericInterface<A>{ @Override public A next() { return null; } }
-
通常使用的通配符包括
T
、K
、V
、E
和?
,它们有一些约定俗成的含义:
?
表示不确定的 Java 类型。T
(type) 表示具体的一个Java类型K
和V
,即key、value,表示Java键值中的健和值。E
(element) 代表
3.2 无界通配符?
- 当传入的类型不确定时,可以使用无界通配符
?
,它可以接收所有未知类型的泛型public void printList(List<?> list){ list.add("2845"); // 编译报错 list.add(null); // 运行时错误,java.lang.UnsupportedOperationException String j = list.get(0); //编译期报错 for (Object obj:list){ System.out.print(obj+" "); } }
?
使用注意事项:
- 只能往使用
List<?>
里添加null
,但运行时会报错。 - 由于不知道
List<?>
中元素的具体类型,只能对其进行读操作,且读取的元素应作为Object
对象进行操作。
3.3 上界通配符<? extends T>
-
<? extends T>
用于表示传入的泛型参数必需是类T或类T的子类/接口T或T的实现类。
-
例如,
<? extends Number>
传入的参数必需是Number
及其子类。List<Integer> list1=Arrays.asList(1,2,3,4); List<String> list2=Arrays.asList("sunrise","john","grace"); getList(list1); getList(list2); // 编译报错,类型不兼容 public static List getList(List<? extends Number> list){ return list; }
-
PESC原则: 上界通配符,一般用于读取的场景。
- 为泛型指定的类型只能是
T
类型或者其子类。 - 只能为其列表添加
null
。 - get方法获取的值只能赋值给
T
类或者其超类。
3.4 下界通配符<? super T>
<? super T>
表示传入的泛型参数必需是类T或类T的父类。
- 例如,
List<? super Integer>
传入的参数必须是Integer
或其父类。List<Number> list=Arrays.asList(12,13,45); getSuperList(list); List<String> list2=Arrays.asList("sunrise","john","grace"); getSuperList(list2); // 编译报错 public static List getSuperList(List<? super Integer> list){ return list; }
- PECS原则: 下界通配符,一般用于写入的场景。
- 为泛型指定的类型必须为
T
,或者其超类。 - 可以为其列表添加任意
T
类型,或者其子类。 - get方法获取的类型,只能赋值给Object类型。
- 总结: PECS原则
- 上界
<? extends T>
不能往里存,只能往外取,适合频繁往外面读取内容的场景。 - 下界
<? super T>
不影响往里存,但往外取只能放在Object对象里,适合经常往里面插入数据的场景。
后续学习: