Java语言基础(SE)-第六节 泛型(Generics)
泛型简介
- 从 Java 5 开始,增加了泛型技术
- 什么是泛型?
- 将类型变为参数,提高代码复用率
-
建议的类型参数名称
T :Type E :Element K :Key N :Number V :Value S、U、V :2nd, 3rd, 4th types
泛型类型(Generic Type)
- 什么是泛型类型?
- 使用了泛型的类或者接口
- 比如
java.util.Comparator
、java.util.Comparable
- 举例使用
-
单个类型参数
public class Student<T> { private T score; public T getScore() { return score; } public void setScore(T score) { this.score = score; } } //main函数 Student<String> stu = new Student<>(); stu.setScore("A"); String score1 = stu.getScore(); Student<Double> stu2 = new Student<>(); stu2.setScore(98.5); double score2 = stu2.getScore();
-
多个类型参数
public class Student<N,S> { private N no; private S score; public Student(N no, S score) { this.no = no; this.score = score; } } //main函数 Student<String, String> s1 = new Student<>("E925", "A+"); Student<Integer, Double> s2 = new Student<>(18, 96.5);
-
泛型类型的继承
-
JDK中的继承
public interface Iterable<T> public interface Collection<E> extends Iterable<E> public interface List<E> extends Collection<E> public class ArrayList<E> implements List<E>
-
示例
Iterable<String> it = null; Collection<String> col = null; List<String> li = null; ArrayList<String> al = null; //多态,父类指向子类 it = col; col = li; li = al; //二者不存在继承关系 List<Object> list = null; ArrayList<String> all = null; //错误 //list = all;
-
示例2:
package com.zh; import java.util.List; public interface MyList<E,T> extends List<E> { void setNo(T no); } //main函数 List<String> li = null; MyList<String, Integer> ml1 = null; MyList<String, Integer> ml2 = null; MyList<String, Integer> ml3 = null; li = ml1; li = ml2; li = ml3;
原始类型(Raw Type)
- 什么是原始类型?
- 没有传递具体的类型给泛型的类型参数 创建的对象。
//Box称为是B<E>的原始类型(Raw Type) Box rawBox = new Box();//warning :rawtypes Box<String> strBox = new Box<>(); rawBox = strBox;//ok strBox = rawBox;//warning :unchecked
- 当使用了原始类型时,编译器会给出 警告(可以用
@SuppressWarnings
消除) - 将非原始类型赋值给原始类型时,编译器没有任何警告和错误
- 将原始类型赋值给非原始类型时,编译器会给出 警告(可以用
@SuppressWarnings
消除) - Box 是原始类型,
Box<Object>
是非原始类型
泛型方法(Generic Method)
泛型方法定义
- 使用了泛型的方法(实例方法、静态方法、构造方法),比如
Arrays.sort(T[], Comparator<T>)
- 方法中的泛型参数不是来自泛型类型
-
泛型方法的格式:
//发型变量声明 方法返回值类型 方法名称(变量类型列表) <泛型变量名称> 方法返回值类型(也可以是泛型变量) 方法名称(参数类型/泛型变量 参数名){}
-
一个类可以不是泛型,但是这个类的某些方法可以支持泛型
//Main类 public static void main(String[] args) { Student<String, String> s1 = new Student<>(); //set方法是Main这个类的类方法,所以可以Main.<String,String>set()调用 //完整写写法 Main.<String,String>set(s1,"k99","C++"); Student<Integer, Double> s2 = new Student<>(); //编译器可以自动推断出类型参数的具体类型 set(s2,25,99.5); } //泛型方法,将参数stu的类型的泛型不要写死,也弄成泛型T1,T2 //注意:泛型方法中使用的泛型参数要在方法的返回值类型签名写上,相当于声明 static <T1,T2> void set(Student<T1,T2> stu,T1 no,T2 score) { stu.setNo(no); stu.setScore(score); }
-
示例
public class Box<E> { private E element; public Box() {} public Box(E element) { this.element = element; } } public static void main(String[] args) { List<Box<Integer>> boxes = new ArrayList<>(); addBox(11, boxes); addBox(22, boxes); addBox(33, boxes); } public static <T> void addBox(T element,List<Box<T>> boxes) { Box<T> box = new Box<>(element); boxes.add(box); }
构造方法
-
举例
public class Person<T> { private T age; //E代表泛型方法的泛型参数,T来自泛型类 public <E> Person(E name,T age){ } } //main函数 Person<Integer> p1 = new Person<>("jack", 20); Person<Double> p2 = new Person<>(666, 20.6); Person<String> p3 = new Person<>(12.34, "80后");
-
注意: 泛型类型带的泛型变量,只能用在构造方法或者实例方法中,不能用在类方法中(泛型类的类型参数使前提是有实例)
public class Box<E> { private E element; public Box() {} public Box(E element) { this.element = element; } //错误,不能用在类方法中 //public static void print(E element){} //泛型方法,这个可以 public static <T> void print(T element){} }
类型推断
```
//类不是泛型类
public class Collections {
@SuppressWarnings("unchecked")
//包含一个泛型方法:<T> 泛型变量声明,List<T>返回值类型
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
}
//根据赋值的变量类型,编译器推断出emptyList的return类型
List<String> list1 = Collections.emptyList();
List<Integer> list2 = Collections.emptyList();
```
限制类型参数
- 可以通过 extends 对类型参数增加一些限制条件,比如
- extends 后面可以跟上类名、接口名,代表 T 必须是 A 类型,或者继承、实现A
- 可以同时添加多个限制,比如 <T extends A & B & C>,代表 T 必须同时满足 A、B、C
- 注意A、B、C,只能最多有一个是类,因为Java不支持多继承。
- 如果有一个是类,那个类必须放在最前面
泛型类使用extends限制
-
举例
//<T extends Number>:T变量限制必须是Number或者其子类 public class Person<T extends Number> { private T age; public Person(T age) { this.age = age; } public int getAge() { return (age ==null) ? 0 : age.intValue(); } } Person<Double> p1 = new Person<>(18.7); System.out.println(p1.getAge()); //18 Person<Integer> p2;//ok //String不是Number的子类型 Person<String> p3;//error
泛型方法使用extends限制
- 泛型方法要在泛型参数声明处使用extends进行限制。
-
示例
//获取数组中的最大值 //<T extends Comparable<T>> 泛型变量声明,同时限制表示T变量类型必须是继承或实现Comparable<T>接口的接口或类 //实现Comparable接口的类能够使用compareTo方法进行比较 static <T extends Comparable<T>> T getMax(T[] array) { if (array == null || array.length == 0) return null; T max = array[0]; for (int i = 0; i < array.length; i++) { if (array[i] == null) continue; //这句话会报错,array[i]是T类型,max也是T类型,不支持比较 //if(array[i] <= max) continue; //实现Comparable接口的对象可以使用compareTo比较 if(array[i].compareTo(max)<= 0) continue; max = array[i]; } return max; } //main函数 //Double、Integer默认实现了Comparable接口 //Double声明:public final class java.lang.Double extends java.lang.Number implements java.lang.Comparable {...} Double[] ds = {5.6,3.4,8.8,4.6}; System.out.println(getMax(ds));//8.8 Integer[] is = {4,19,3,28,56}; System.out.println(getMax(is));//56
综合举例
- 上面的Double、Integer是默认实现了Comparable接口,才可以使用getMax,那么我们是否可以自定义一个类型,也能使用getMax方法呢?
- 那个么这个自定义的类型,需要实现Comparable接口
//Comparable<T>比较的类型是Student<T>,所以是Comparable<Student<T>>
public class Student<T extends Comparable<T>> implements Comparable<Student<T>> {
//如果score也具备可比较性,因此T也要实现Comparable,所以T extends Comparable<T>
private T score;
public Student(T score) {
this.score = score;
}
@Override
public int compareTo(Student<T> s) {
if(s == null) return 1;
if (score != null && s.score != null) return score.compareTo(s.score);
if (score == null && s.score ==null) return 0;
return s.score == null ? 1 : -1;
}
@Override
public String toString() {
return "[score=" + score + "]";
}
}
//main函数
Student<Integer>[] stus = new Student[3];
stus[0] = new Student<Integer>(18);
stus[1] = new Student<Integer>(18);
stus[2] = new Student<Integer>(18);
//[score = 38]
System.out.println(getMax(stus));
心得总结
- 泛型T使用了extends之后,会让代码更加难以读懂
- 无论是泛型类还是泛型方法中使用extends限制泛型参数T,可以把
T extends A
就看成是T,先不考虑限制来读取代码。然后真正使用这个T参数时再考虑T需要满足什么限制。
通配符(Wildcards)
- 在泛型中,问号(?)被称为是通配符
- 通常用作变量类型、返回值类型的类型参数
- 一般是用于定义一个引用变量,以便实现“多态”调用(非真正意义上的多态)。
-
定义为引用变量,可指向不同类型的变量
//使用? SuperClass<?> sup = new SuperClass<String>("lisi"); sup = new SuperClass<People>(new People ()) ; sup = new SuperClass<Animal>(new Animal()) ; //使用具体类型 SuperClass<String> sup1new SuperClass<String>(ulisin) : SuperClass<People> sup2new SuperClass<People>; SuperClass<Animal> sup3 = new SuperClass<Animal>:
-
或者方法形参用于接收不同参数,实际也是一个引用变量,如下:
List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); l1.add("AAA"); l2.add(222); public static void test1(List<?> l1){ String join = Joiner.on(",").join(l1); System.out.println(join); }
-
-
不能用作泛型方法调用、泛型类型实例化、泛型类型定义的类型参数
//泛型类型实例化,不能出现 //Box<Integer> p1 = new Box<?>(); //泛型方法调用,不能出现 //Main2.<?>set(s1,"k99","C++"); //类型定义,不能出现 //public class Person<? extends Number> {}
上界
-
可以通过 extends 设置类型参数的上界
//类型参数必须是Number类型或者是Number的子类型 static void testUpper(Box<? extends Number> box) {} Box<Integer> p1 = null; Box<Number> p2 = null; //可以不指定类型,用?代替 Box<? extends Number> p3 = null; //Integer是Number子类型,所以可以 Box<? extends Integer> p4 = null; testUpper(p1); testUpper(p2); testUpper(p3); testUpper(p4);
-
示例代码
static double sum(List<? extends Number> list) { double s = 0.0; for (Number n : list) { s += n.doubleValue(); } return s; } //Arrays.asList创建一个List集合 List<Integer> is = Arrays.asList(1,2,3); //6.0 System.out.println(sum(is)); List<Double> ds = Arrays.asList(1.2,2.3,3.5); //7.0 System.out.println(sum(ds));
下界
-
可以通过 super 设置类型参数的下界
//类型参数必须是Integer类型或者是Integer的父类型 static void testLower(Box<? super Integer> box) {} Box<Integer> p1 = null; Box<Number> p2 = null; Box<? super Integer> p3 = null; //Number是Integer的父类型 Box<? super Number> p4 = null; testLower(p1); testLower(p2); testLower(p3); testLower(p4);
-
示例
static void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 10; i++) { list.add(i); } } List<Integer> is = new ArrayList<>(); addNumbers(is); //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] System.out.println(is); //Number是Integer的父类 List<Number> ns = new ArrayList<>(); addNumbers(ns); //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] System.out.println(ns);
无限制
-
举例:
//类型参数是什么类型都可以 public static void test(Box<?> box) {}; //main函数 //什么类型都可以 Box<Integer> p1 = null; Box<String> p2 = null; Box<Object> p3 = null; Box<? extends Number> p4 = null; Box<? super String> p5 = null; Box<?> p6 = null; test(p1); test(p2); test(p3); test(p4); test(p5); test(p6);
-
示例
static void printList(List<?> list) { for (Object obj : list) { System.out.print(obj + " "); } System.out.println(); } List<Integer> is = Arrays.asList(1,2,3); printList(is);//1 2 3 List<Double> ds = Arrays.asList(1.2,2.3,3.5); printList(ds);//1.2 2.3 3.5
无限制通配符和有限制通配符的数据读写问题
1. 无限制通配符读写
- 读:可以读,尽管被无限制通配符修饰,但不管如何,容器里的元素永远是一个对象,也就是一个Object,所以可以用Object o这个引用变量来读取容器元素,即利用对象多态性。
-
写:不可以往容器里写入元素,因为写入的元素与容器的具体类型的关系不明确。比如:可以定义成接收变量,但无法向里面添另数据。
public static void main(strings[] args) { ArrayList<?> list = new ArrayList<String>(); list = new ArrayList<Integer>(); list.add(1); }
- list可以指向不同的具体容器,相对应的能接收的元素类型也跟着改变。因此往容器里添加数据时,会因为无法确定所能添加的具体元素类型为何,导致的类型不安全而编译不通过。
- 注意: 可以添加null,因为null是所有类型的成员。
2. 有限制通配符读写
? extends A
:- 读:往容器中添加的元素类型,一定是A或A的子类(如果是接口,则为实现类),因此读取出来的数据,一定可以用A类型来做引用(当然Object也可以)。
-
写:由于容器的具体类型未知,如果往容器添加元素,无法确保添加进去的具体数据是该容器具体类型的子类还是父类,因此存在类型不安全问题,所以是不允许往里添加数据的。null可以添加。
static void swapFist(List<? extends Number> l1,List<? extends Number> l2) { Number temp = l1.get(0); //ok l1.set(0, l2.get(0));//error l1.set(0, temp);//error }
? super A
:- 读:不管容器里添加的元素是A还是A的父类的实例对象,它们始终都是Object对象,因此可以读取,用Object类型来做引用(不能用A类型来做引用)。
- 写:只能写入A类型及其子类的实例对象。因为具体容器的类型最低等级是到A,所以不管容器具体类型为何,它都能保证>= A,所以A的实例化对象可以被写入(对象多态性),当然,既然作为父类的A可以被写入,那么A的子类自然而然也可以被写入容器里了。null可以添加。
通配之间的继承关系
T与?区别
- T代表一种类型,?是通配符,泛指所有类型
- 在编译成字节码calss文件时,T会进行替换成实际类型,?不会替换类型,可以认为?不属于泛型,跟泛型没有关系
- 泛型(T)本质是将数据类型参数化,它通过擦除的方式来实现。声明了泛型的 .java 源代码,在编译生成 .class 文件之后,泛型相关的信息就消失了。可以认为,源代码中泛型相关的信息,就是提供给编译器用的。泛型信息对 Java 编译器可以见,对 Java 虚拟机不可见。
- Java 编译器通过如下方式实现擦除:用 Object 或者界定类型替代泛型,产生的字节码中只包含了原始的类,接口和方法;在恰当的位置插入强制转换代码来确保类型安全;在继承了泛型类或接口的类中插入桥接方法来保留多态性。
-
泛型定义方法时需要在方法前声明泛型参数,通配符不需要,而且仅仅用来用作方法的参数类型。
static void foo1(List<?> list) {} //声明泛型参数<T> static <T> void fooHelper(List<T> l) {}
- 使用范围不同:
- ? 通配符用作 参数类型、字段类型、局部变量类型,有时作为返回类型
- T 用作 声明类的类型参数、通用方法的类型参数
- 举例
-
例1;
//编译器在解析 `List<E>.set(int index, E element) `时,无法确定E的真实类型,所以报错 static void foo(List<?> list) { Object obj = list.get(0);//ok //list.set(0,obj);//error //list.set(0,list.get(0)); //error }
-
例2:解决上述问题
static void foo1(List<?> list) { fooHelper(list);//ok } //使用泛型T,编译器编译的时候会进行全局替换,T直接被替换成Object static <T> void fooHelper(List<T> l) { //l.get(0)直接编译成Object类型实例,即一个指针 l.set(0,l.get(0)); //ok }
-
泛型的使用限制
-
基本类型不能作为类型参数
//error Map<int, char> map1 = new HashMap<>(); //ok Map<Integer, Character> map2 = new HashMap<>();
-
不能创建类型参数的实例
public class Box<E> { public void add(Class<E> cls) throws Exception{ //error,类型参数的实例 E e1 = new E(); //ok E e2 = cls.newInstance(); } }
-
不能用类型参数定义静态变量
public class Box<E> { //error private static E value; } Box<Integer>[] box1 = new Box(); Box<String>[] box2 = new Box(); //请问静态变量value是什么类型? Integer还是String?
-
泛型类型的类型参数不能用在静态方法上
public class Box<E> { public static void show(E value) {} }
-
类型参数不能跟 instanceof 一起使用
ArrayList<Integer> list = new ArrayList<>(); //error if (list instanceof ArrayList<Integer>) {}
-
不能创建带有类型参数的数组
//error Box<Integer>[] boxes1 = new Box<Integer>[4]; //OK Box<Integer>[] boxes2 = new Box[4];
-
下面的方法不属于重载
//error void testX(Box<Integer> box) {} void testX(Box<String> box) {} //error void foo(Box<? extends Number > box) {} void foo(Box<String> box) {}
-
不能定义泛型的异常类
//error,异常子类不能有类型参数 public class MyException<T> extends Exception {}
-
catch 的异常类型不能用类型参数
public static <T extends Exception> void test(Box<T> box) { try { } catch (T e) { //error,catch右边不能有类型参数 } }
-
下面的代码是正确的
public class Parser <T extends Exception> { //OK,异常抛出可以写T public void parse(File file) throws T{ } }