泛型
泛型是一种参数化类型的机制。它可以使得代码适用于各种类型,从而编写更加通用的代码
- 将原来具体的类型,变成一个可变的参数类型,使用时传递什么样的类型参数它就是什么类型。类似于方法中的变量参数。
- 泛型是一种编译时类型确认机制。它提供了编译期的类型安全。确保在泛型类型上只能使用正确类型的对象,避免了在运行时出现的ClassCastException
- 泛型使多种数据类型都可以执行一个代码,提高了代码的复用性
泛型的引入
我们知道集合中可以存放任意类型的对象。只要把对象放入集合中,其实这时对象都被提升为一个Object类型(隐藏的向上转型),那么这个时候,我们想要使用这个对象时,就需要进行向下转型,来还原这个对象,然后再进行使用
public class Test1{
public static void main(String[]args){
Collection c1 = new HashSet();
//自定义一个类,对象有三个属性:学号、姓名、年龄
c1.add(new Student(1001,"张三",18));
c1.add(new Student(1002,"李四",19));
c1.add(new Student(1003,"王五",22));
for (Object o:c1){
//使用之前需要先进行向下转型
Student student = (Student) o;
System.out.println(student.getName());
}
}
}
在JDK5之后新增了泛型语法。在设计API时就可以指定类或者方法支持泛型,在我们使用这个类时就可以指定数据类型,这样也可以避免取出对象时发生ClassCastException。
当我们在定义集合时加上<泛型类型>,就可以给这个集合指定存入哪一种数据类型,那么放入集合中的对象也不会自动向上转型,我们也不需要先向下转型后再使用集合中的元素。
public class Test2{
public static void main(String[] args){
//指定集合中存入数据的类型
Collection<Student> c2 = new HashSet<>();
c2.add(new Student(1001,"小明",20));
c2.add(new Student(1002,"小红",21));
c2.add(new Student(1003,"小王",22));
//明确集合中的元素类型,也不需要Object向下转型
for (Student s: c2){
System.out.println(s.getName());
}
}
}
泛型类、泛型接口
定义泛型类、接口
我们可以为任何类和接口增加泛型声明,并不是只有集合类才可以使用泛型声明
- 泛型形参的命名一般使用单个的大写字母,如果有多个类型形参,那么使用逗号隔开,如Map<K,V>
- 常见的字母表示:
- T:Type
- K,V:Key,Value
- E:Element
举例:
定义学生类,其中学生的成绩可以为整数、小数、字符串(优、良、中、差)
class Student<T>{
private String name;
//分数的数据类型就为T
private T score;
//构造方法不需要加类名中的<T>
public Student(){
}
public Student(String name, T score) {
this.name = name;
this.score = score;
}
//静态方法和静态属性不能使用泛型
// public static T abs;
// public static void sta(T score){ }
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public T getScore() {
return score;
}
public void setScore(T score) {
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", score=" + score +
'}';
}
}
public class Test11 {
public static void main(String[] args) {
//泛型类型必须是引用数据类型
Student<Integer> s1 = new Student<>("张三",100);
System.out.println(s1);
Student<String> s2 = new Student<>("李四","优秀");
System.out.println(s2);
}
}
通过上面的代码发现,泛型的使用有很多规则
泛型的约束和局限性
- 在定义类或接口时,指定类型形参,类型形参在整个类或接口体中可以当成类型使用,几乎所有可能使用其他普通类型的地方都可以使用这种类型形参,如:属性类型、方法的形参类型,方法返回值类型
- 但是泛型类或者泛型接口上的泛型形参(变量),不能用于声明静态变量,也不能用在静态方法中,那是因为静态成员的初始化是随着类初始化的,此时泛型实参没有指定。
- 我们在声明变量就要指定泛型实参,
- 泛型实参必须是引用数据类型,不能是基本数据类型
- 无法使用instanceof关键字,或者= =判断泛型类的类型
- 泛型数组可以声明但是无法实例化
- 泛型类不能继承Exception或者Throwable
- 不能捕获泛型类型限定的异常,但是可以将泛型限定的异常抛出
创建泛型类对象的写法:
- JDK1.5引入泛型:
Student<Integer> s1 = new Student<Integer>;
- 在JDK1.7后支持:
Student<Integer> s1 = new Student<>;
在继承泛型类或实现泛型接口时,子类不延续使用该泛型,那么要明确指定实际类型,此时子类就不再是泛型类了
interface MyList extends List<String>{}
如果延续使用泛型:
延续使用父类、父接口的泛型形参
如果要继承泛型类、实现泛型接口时,想要保留父类(父接口)的泛型,必须在父类(父接口)、子类(子接口)中都要保留
//继承有泛型的类,子类和父类泛型形参要一致,即使List类中泛型形参是E
interface MyList2<T> extends List<T>{ }
系统定义的集合类:
public interface List<E> extends Collection<E> {···}
···
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{···}
设定泛型类的上限
刚刚我们为学生类的成绩使用了泛型形参,但是此时成绩的类型可以是任何类型,如果我们想要成绩只为数字类型(Integer、Double、Float···)怎么办呢?
//修改<>里面的内容就可以为泛型形参设定上限,从而缩小了它的类型范围
class Student<T extends Number>{
···
为泛型形参设定了上限后,泛型形参只能为 他自己(Number)或者他的子类(Integer、Double、Float···)
泛型形参至多只能有一个父类上限,但是可以有多个接口上限。
在一种更加极端的情况下,程序要为泛型形参设置更多的限定条件。该泛型形参不光要在父类上限范围内,还要实现了规定的上限接口
//多个接口上限,中间使用& ,类上限必须在前面(和类同时继承类和接口一样)
class MyNumber2<T extends Number & java.io.Serializable>{ }// 表示这个类必须是上限类或者其子类并且实现了所有上限接口
泛型方法
- 两个问题:
- 如果我们定义类或接口时,没有使用泛型的类型形参,但是某个方法想要自己来定义类型形参
- 前面讲到类和接口的类型形参不能用在静态方法中,但是我们的静态方法也想要自己来定义类型形参
- 为了解决上面的两个问题,JDK1.5之后还提供了泛型方法
定义泛型方法
格式:
[修饰符] <泛型形参列表> 返回值类型 方法名([形参列表]) 抛出的异常列表{}
举例:
我们想要将一个数组中的元素添加到一个集合中去,我们想让所有类型的数组都可以使用这个方法,那么就要给这个数组和集合一个类型实参,并且把这个方法要声明为泛型方法。
public class Test13 {
public static <T> void fromCollection(T[] a, Collection<T> c) {
for(T t :a){
c.add(t);
}
}
public static void main(String[] args){
Collection<Integer> c1 = new ArrayList<>();
Integer[] ints = {1,2,4};
fromCollection(ints,c1);
System.out.println(c1);
Collection<String> c2 = new ArrayList<>();
String[] strs = {"a","aa","aaa"};
fromCollection(strs,c2);
System.out.println(c2);
}
}
- 泛型形参列表,可以是一个也可以是多个,如果是多个,使用逗号分隔,
- <泛型形参列表> 必须在修饰符和返回值类型之间
- 泛型方法中声明的泛型形参只能在当前方法中使用,和其他方法无关
- 泛型方法申明中定义的泛型形参无需显示传入实际类型参数,编译器会根据实际参数的类型自动推断出形参的实际类型
设定泛型方法的上限
其实没有设置泛型形参上限的,可以把它的上限看成Object
//这样这个方法传入的实参只能是Number类型数组或者其子类数组了
public static <T extends Number> void fromCollection(T[] a, Collection<T> c) {···
和泛型类的上限规则一样
类型通配符
当我们声明一个方法时,某个形参类型是一个泛型类或者泛型接口,但是在声明方法时,又不确定该泛型实际类型,我们就可以考虑使用类型通配符
定义类型通配符
举例:
写一个方法操作List中的元素
- 当我们不确定一个LIst接口中的形参类型是什么的时候,如果我们这个方法并不需要对List进行添加操作只做读取,就可以不使用泛型方法,而使用类型通配符来实现
//声明泛型形参时,设置类型通配符
public void test1(List<?> list){
//这种设置过的泛型,使用时只能获取其中的值,但是不能为它添加值
//我们使用泛型通配符后,我们也不知道它其中的类型是什么,所以不能向里面添加
// list.add(100);
list.add(null);//null可以,因为他是所有引用数据类型的实例
for (int i=0;i<list.size();i++){
//这里list.get()方法提示的返回值也是不确定的类型(capture of ?)
System.out.println(list.get(i));
}
}
- 如果我们这个方法需要向list中添加元素,我们可以使用泛型方法
public <T> void test11(List<T> list,T t){
//可以向list中添加一个T类型的元素t
list.add(t);
//可以遍历读取list中的元素
for (T t1 : list) {
System.out.println(t);
}
}
设定类型通配符的上限
使用类型通配符后,那么这个List接受所有元素类型的List,当我们想要这个方法只接受一定类型的List集合,同样也可以为类型通配符设定上限(<? extends Parent> )
//设置类型通配符上限
public void test2(List<? extends Number> list){
//(只有Number和他的子类类型的集合才能使用这个方法)
// list.add(100); //此时也不可以为list添加值,因为就算为传入的集合类型设置了范围,也不能确定究竟是哪一个类
//Number类下还有Integer、Double、等,所以使用类型通配符还是不能向集合中添加元素
//如果我们想要添加,建议就不要使用类型通配符了,要使用泛型方法来实现
}
设定类型通配符的下限
为了约束这个不确定集合中的元素类型,Java中允许类型通配符设置下限(<? super Child>)
//设置类型通配符下限
//设置下限的List,list中的元素只能是下限类或者下限类的父类
public void test3(List<? super Number> list){
}
注意:只有类型通配符才可以设置下限, 泛型形参是不可以的!
泛型擦除
在严格的带泛型声明的类里,最好要带上类型参数,但为了与老的java代码保持一致,所以带泛型的类也可以不指定泛型类型。此时的类,它的泛型属于原始类型(raw type),默认是该类型形参的上限,如果没有上限就为Object
java中泛型在运行期是不可见的,会被擦除为它的上级类型,如果没有没有限定的泛型参数类型,就被替换为Object
//其中设置的泛型String被擦除
ArrayList list = new ArrayList<String>();
解释:
解释上面提到的泛型的约束和局限性,结合着来看:
并不存在泛型类型的Class对象
我们通过泛型使得一个类型的功能增强了,好像扩展出好多子类一样,比如一个集合可以使存储Integer的类,也可以是存储String的类,但系统并没有为ArrayList、ArrayList生成一个新的class文件,也不会把它们当成一个新的类,他们就是ArrayList类
ArrayList<Integer> list1 = new ArrayList<>();
ArrayList<String> list2 = new ArrayList<>();
System.out.println(list1.getClass());
System.out.println(list2.getClass());
//并不能判断这个泛型类的泛型,返回true
System.out.println(list1.getClass()==list2.getClass());
if (list1 instanceof ArrayList){
System.out.println("list1属于ArrayList");
}
//系统中并不会真正生成泛型类,所以
//这种写法编译就会报错
// if (list1 instanceof ArrayList<String>){
//
// }
泛型与数组
java中是“不能创建一个确切的泛型类的数组“的
也就是说数组的元素不能包含类型变量或类型形参,除非是无上限的类型通配符
//报错
// List<String>[] ls1 = new ArrayList<String>[10];
//只能声明这样是数组,但是不能创建这样的对象
List<String>[] ls3 = new ArrayList[10];
//可以使用类型通配符
List<?>[] ls2 = new ArrayList<?>[10];
使用通配符的方式,最后取出的数据要做显示的类型转换(向下转型)
List<?>[] arr = new List<?>[2];
arr[0] = Arrays.asList("hello","java");
arr[1] = Arrays.asList(1,2,3);
for (List<?> list : arr) {
for (Object object : list) {
System.out.println(object);
}
}
泛型与异常
/**
* 泛型类不能继承Exception或者Throwable
* Generic class may not extend 'java.lang.Throwable'
*/
// private class MyGenericException<T> extends Exception {
// }
// private class MyGenericThrowable<T> extends Throwable {
// }
/**
* 不能捕获泛型类型限定的异常
* Cannot catch type parameters
*/
public <T extends Exception> void getException(T t) {
// try {
//
// } catch (T e) {
//
// }
}
/**
*可以将泛型限定的异常抛出
*/
public <T extends Throwable> void throwsException(T t) throws T {
try {
} catch (Exception e) {
throw t;
}
}
泛型的嵌套
Map<Integer,String> map = Map.of(1,"a",2,"b");
Set<Map.Entry<Integer,String>> entrySet = map.entrySet();