@toc
1、泛型的概念
java语言的多态性让我们可以把某些只能在运行时确定的类型在编译时使用父类或者父接口表示,这确实解决了很多问题。但有时程序员在声明某些变量时不知道它的具体父类或父接口,只能选择公共父类Object类型,这很不方便。
为了解决这个问题,JDK1.5引入了泛型的概念,让我们可以在程序中用某种方式表示完全未知的类型,使得程序顺利编写并通过编译,等到使用时再确定它的具体类型。==泛型(Generics)指的就是泛化的类型,即用<T>
来表示一个未确定的类型。==
2、泛型类或泛型接口
如果某个类或接口在声明时,在类名和接口名后面加了泛型,那么就称它为泛型或泛型接口。JDK1.5把所有的集合类和接口都改写成了泛型类和泛型接口。
示例代码:
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
public class HashMapTest {
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<>();
map.put(1,"codeleader1");
map.put(2,"codeleader2");
map.put(3,"codeleader3");
Set<Integer> keySet = map.keySet();
Iterator<Integer> iter = keySet.iterator();
while (iter.hasNext()){
Integer key = iter.next();
System.out.println(key+"->"+map.get(key));
}
}
}
2.1 泛型类或接口的声明
我们可以为任何类或接口增加泛型声明,并不是只有集合才能使用。泛型形参的命名一般使用单个的大写字母,如果有多个类型形参,那么中间使用逗号分隔,如Map<K,V>
。
案例需求:现在需要定义一个学生类,这个学生类的成绩可以是如下几种类型:
- 整数
- 小数
- 字符串“优秀、良好、合格、不及格”
这就意味着学生的成绩类型是不确定的,因此在声明学生类时,成绩类型要用<T>
等泛型字母表示。
示例代码:
public class Student<T> {
private String name;//姓名
private T score;//成绩
public Student(String name, T score) {
this.name = name;
this.score = 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 StudentTest {
public static void main(String[] args) {
Student<Integer> s1 = new Student<Integer>("张三", 95);
Student<String> s2 = new Student<String>("张三", "优秀");
Student<Double> s3 = new Student<Double>("张三", 80.5);
}
}
从上面的代码可以看出,但我们把学生成绩的类型用泛型<T>
表示后,每次创建学生对象时可以由程序员指定成绩的具体类型,如<Integer>、<String>、<Double>
等。
定义在类或接口上的泛型类型,在整个接口或类体中可以当成普通类型使用,如<T>
可以用来表示属性类型、方法的形参类型、方法返回值类型等。
但是请注意==泛型类或泛型接口上声明的泛型<T>
等,不能用于声明静态变量,也不能用在静态方法中,因为静态成员的初始化是随着类的初始化而初始化的,此时泛型的具体类型还无法确定,那么泛型形参的类型就不确定,所以不要在静态成员上使用类或接口上的泛型形参类型==。
2.2 泛型类或接口的使用
当某个类或接口声明了泛型后,在使用它们时,应该尽量为其泛型指定具体的类型。
另外,泛型实参类型的指定也有要求,它必须是引用数据类型,不能是基本数据类型,并且泛型类或接口后面声明了几个泛型,在使用时就要指定几个具体类型。
一般在什么时候指定泛型的具体类型?
在用泛型类、接口声明变量并创建对象时,可以指定泛型的具体类型。
ArrayList<String> list=new ArrayList<>();
- 在继承泛型类或实现泛型接口时,如果子类不延续使用该泛型,那就必须指定实际类型,此时子类不再是泛型类了。
2.3 延续父类或接口的泛型
如果在继承泛型类或实现泛型接口时,想要继续保留父类或父接口的泛型,那么必须在父类、父接口和子类、子接口中都要保留泛型。
例如,声明一个子类SubArrayList,让它继承ArrayList<E>
,但又希望SubArrayList仍然是一个泛型类,而且该泛型类仍然表示未知的元素类型。
示例代码:
//SubArrayList<E>和ArrayList<E>保持一样的泛型字母即可
class SubArrayList<E> extends ArrayList<E>{
}
2.4 设定泛型的上限
假如我们有一个新需求,要求学生类的成绩仍然是未确定的类型,但它必须是如下的数字类型之一,不能是String等其他非数字类型。
- Integer
- Float
- Double
- ...
示例代码:
public class Student<T extends Number> {
private String name; //姓名
private T scode; //成绩
public Student(String name, T scode) {
this.name = name;
this.scode = scode;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public T getScode() {
return scode;
}
public void setScode(T scode) {
this.scode = scode;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", scode=" + scode +
'}';
}
}
测试类代码:
public class StudentUpperBoundTest {
public static void main(String[] args) {
Student<Integer> s1 = new Student<>("张三", 95);
Student<Double> s2 = new Student<Double>("张三", 80.5);
//编译报错,String不是Number的子类
// Student<String> s3 = new Student<String>("张三", "优秀");
}
}
上面的示例代码将Student类的泛型<T>
设定了上限<T extends Number>
,那么就意味着<T>
只能指定为Number或其子类。如果此时<T>
指定为<String>
或其他非Number系列的类,那么编译器就会报错。
==如果泛型形参没有设定上限,那么泛型实参可以是任意引用数据类型,相当于默认上限是Object;如果泛型形参设定了上限(如T extends 父类上限
),那么就只能指定为该父类本身或其子类型。==
在一种更极端的情况下,程序需要为泛型设定多个上限,那么多个上限之间用&
符号进行连接,并且规定在这多个上限中,至多有一个父类上限,但可以有多个接口上限,表明该类型形参必须是其父类的子类(包括其父类本身),并且可以实现多个上限接口,父类在前接口在后。
示例代码:
class Student<T extends Number & Cloneable>{
//...省略其他代码
}
上面的示例代码表示该Studeng类的<T>
类必须指定为一个既继承了Number父类又实现了Cloneable父接口的类型。
2.5 案例:矩形对象管理
声明一个矩形类(Rectangle),包含长和宽,现在要求矩形类实现java.lang.Comparable<T>
接口,指定T为Rectangle类,重写抽象方法,按照矩形的面积大小排序。在测试类中创建Rectangle数组,然后调用Arrays.sort(Object[] arr)
方法进行排序,遍历显示矩形对象信息。
Rectangle类示例代码:
public class Rectangle implements Comparable<Rectangle> {
private double length;//长
private double width;//宽
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
//计算矩形的面积
public double area(){
return length*width;
}
@Override
public int compareTo(Rectangle o) {
return Double.compare(area(),o.area());
}
@Override
public String toString() {
return "Rectangle{" +
"length=" + length +
", width=" + width +
"area="+area()+
'}';
}
}
测试类:
import java.util.Arrays;
public class RectangleTest {
public static void main(String[] args) {
Rectangle[] arr = new Rectangle[3];
arr[0] = new Rectangle(1, 4);
arr[1] = new Rectangle(2, 2);
arr[2] = new Rectangle(1, 3);
Arrays.sort(arr);
for (Rectangle rectangle : arr) {
System.out.println(rectangle);
}
}
}
3、泛型方法
JDK1.5允许在类或接口上声明泛型,还允许单独在某个方法签名中声明泛型,这样的方法称为泛型方法。泛型方法可以是静态方法,也可以是非静态方法。
3.1 泛型方法的声明
现在需要声明一个方法fromArrayToCollection(),该方法的功能是将一个对象数组的所有元素添加到一个对应类型的Collection集合。
public class TestMethod2 {
public static <T> void fromArrayToCollection(T[] a, Collection<T> c){
for (T t : a) {
c.add(t);
}
}
public static void main(String[] args) {
String[] strings={"hello","world","java"};
ArrayList<String> stringList = new ArrayList<>();
fromArrayToCollection(strings,stringList);
for (String s : stringList) {
System.out.println("s="+s);
}
Integer[] integers={1,2,3,4,5};
ArrayList<Integer> integerList = new ArrayList<>();
fromArrayToCollection(integers,integerList);
for (Integer integer : integerList) {
System.out.println("integer="+integer);
}
}
}
我们在调用fromArrayToCollection方法的时候没有手动指定<T>
的具体类型,这是因为编译器可以根据传入的实参自动进行类型推断。
泛型方法的语法格式如下所示:
【修饰符】 <泛型> 返回类型 方法名([形参列表]) 抛出的异常列表{
//方法体...
}
其中<泛型>
中的类型,可以是一个或多个,如果是多个就用逗号分隔,和定义泛型类、泛型接口时一样,而且<泛型>
必须声明在修饰符和返回值类型之间。
==与泛型类、泛型接口声明中定义的<泛型>
不同,当前方法声明的<泛型>
只能在当前方法中使用,和其他方法无关。另外,方法声明中定义的<泛型>
不需要显示传入具体的类型参数,编译器可以根据调用方法时实参的类型自动推断。==
3.2 设定泛型形参的上限
在声明泛型类或泛型接口时,<泛型>
是可以指定上限的,同样在声明泛型方法时,<泛型>
也可以指定上限,这两种的语法格式和要求是一样的。如果没有指定上限,则默认上限为Object,如果有多个上限,则用&
连接,并且父类在前,父接口在后,至多只能指定一个父类上限。
案例需求:有一个图形的抽象类(Graphic),包含抽象方法double getArea,返回图形面积。其中两个子类是圆型Circle和矩形Rectangle。现在需要声明一个pringArea方法可以遍历打印Collection系列集合中多个图形的面积。Collection系列集合的泛型可能是<Rectangle>、<Circle>、<Graphic>
等图形类型,但不能是其他非图形类型。
案例分析:pringArea方法的形参应该是Collection<T>
,因为具体是什么图形也不确定,但是<T>
需要又范围限制,必须是Graphic或其子类,因此在声明T时可以加上限,即<T extends Graphic>
。
Graphic父类代码:
public abstract class Graphic {
public abstract double getArea();
}
Circle类代码:
public class Circle extends Graphic {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
//重写getArea方法
@Override
public double getArea() {
return Math.PI*radius*radius;
}
@Override
public String toString() {
return "Circle{" +
"radius=" + radius +
'}';
}
}
Rectangle类代码:
public class Rectangle extends Graphic {
private double length;
private double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
//重写getArea方法
@Override
public double getArea() {
return length*width;
}
@Override
public String toString() {
return "Rectangle{" +
"length=" + length +
", width=" + width +
'}';
}
}
测试类代码:
import java.util.ArrayList;
import java.util.Collection;
public class GraphicTest {
//泛型方法,设定泛型上限为Graphic,则<T>必须是Graphic或其子类
public static <T extends Graphic> void printArea(Collection<T> graohics){
for (T t : graohics) {
System.out.println(t.getArea());
}
}
public static void main(String[] args) {
ArrayList<Circle> cList = new ArrayList<>();
cList.add(new Circle(1.2));
cList.add(new Circle(2.3));
printArea(cList);
ArrayList<Rectangle> rList = new ArrayList<>();
rList.add(new Rectangle(1,2));
rList.add(new Rectangle(2,3));
printArea(rList);
ArrayList<Graphic> gList = new ArrayList<>();
gList.add(new Circle(1.2));
gList.add(new Rectangle(2,3));
printArea(gList);
}
}
4、类型通配符
当声明一个方法的某个形参类型是一个泛型类或泛型接口,但是不确定该泛型的实际类型时,如某个方法的形参类型是ArrayList<E>
,实参集合元素可能是任意类型,即此时形参无法将<E>
具体化。Java提供了类型通配符来解决这个问题。使用泛型类或泛型接口的类型声明其他变量时也是如此。
4.1 类型通配符的使用
类型通配符用一个<?>
来表示,它代表任意引用数据类型。类型通配符只能出现在泛型类或泛型接口来声明变量或形参时。
案例需求:声明一个disjoint方法,如果两个指定的Collection集合没有共同的元素,则返回true,否则返回false。
public class WildcardAnyTest {
public static boolean disjoint(Collection<?> c1,Collection<?> c2){
for (Object o1 : c1) {
for (Object o2 : c2) {
if(o1.equals(o2)){
return false;
}
}
}
return true;
}
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
list1.add("hello");
list1.add("code");
list1.add("leader");
ArrayList<String> list2 = new ArrayList<>();
list2.add("1");
list2.add("2");
list2.add("3");
ArrayList<Integer> list3 = new ArrayList<>();
list3.add(1);
list3.add(2);
list3.add(3);
System.out.println(disjoint(list1,list2));
System.out.println(disjoint(list2,list3));
}
}
4.2 类型通配符的上限
<T>
在声明时可以通过<T extends Type>
的方式限定"T"的上限,<?>
同样可以通过<? extends Type>
的方式限定"?"的上限,<?>
代表任意的引用数据类型,<? extends Type>
代表泛型类型必须是类型本身,或者是类型的子类。
4.3 类型通配符的下限
==我们在声明<T>
时之可以通过<T extends 上限>
的形式指定其上限。但是在使用<?>
时,既可以通过<? extends 上限>
的方法指定其上限,还可以通过<? super 下限>
的方式指定其下限==。
案例需求:假设需要声明一个处理两个Collection集合的静态方法,它可以将src集合中的元素剪切到dest集合中,并且返回被剪切的最后一个元素。
案例分析:public static <T> T cut(Collection<? super T> dest,Collection<T> src )
。可以表示依赖关系,不管src集合元素类型中的T是什么,只要dest集合元素的类型是T或T的父类即可。而且如果此时dest的泛型是<Object>
,src的泛型是<String>
,那么cut方法返回的结果是String类型,完美地记录了源集合src的元素类型。
public class WildcardSuperTest {
//将src集合中的元素剪切到dest集合中,并且返回被剪切的最后一个元素
public static <T> T cut(Collection<? super T> dest,Collection<T> src){
T last=null;
Iterator<? extends T> iterator = src.iterator();
while(iterator.hasNext()){
T next = iterator.next();
last=next;
dest.add(next);
iterator.remove();
}
return last;
}
public static void main(String[] args) {
ArrayList<Circle> cList = new ArrayList<>();
cList.add(new Circle(1.2));
cList.add(new Circle(2.3));
ArrayList<Graphic> gList = new ArrayList<>();
Circle lastCut=cut(gList,cList);
System.out.println("lastCut="+lastCut);
ArrayList<Rectangle> rList = new ArrayList<>();
rList.add(new Rectangle(1,2));
rList.add(new Rectangle(2,3));
Rectangle trailCut=cut(gList,rList);
System.out.println("trailCut="+trailCut);
}
}
4.4 泛型方法与类型通配符
根据前面几个小节,得出以下结论:
<?>
可以代表任意类型,<? extends Type>
可以代表Type或Type的子类,<? super Type>
可以代表Type或Type的父类。<T>
可以代表任意类型,<T extends Type>
可以代表Type或Type的子类。
案例需求:声明一个joinIfAbsent方法,实现如果某个元素在指定Collection集合中不存在,那么就将这个元素添加到集合中。
import java.util.ArrayList;
import java.util.Collection;
public class GenericTypeMethodTest3 {
public static <T> void joinIfAbsent(Collection<? super T> coll,T t){
if(!coll.contains(t)){
coll.add(t);
}
}
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
joinIfAbsent(list,"code");
for (String s : list) {
System.out.println("s="+s);
}
}
}
4.5 泛型擦除
在严格的泛型代码中,使用泛型类和泛型接口时,就应该明确<泛型>
指定具体类型。但为了与旧的Java代码保持一致,所以也允许在使用泛型类和泛型接口时不指定具体类型,这种情况称为泛型擦除。
如果没有为泛型类的<泛型>
指定具体类型,则该类被称作原始类型,此时<泛型>
自动按照<泛型>
的第一个上限类型处理。==如果某个泛型没有指定上限,则默认上限是Object,即泛型擦除后,<泛型>
自动按照第一个上限处理。==
import java.util.ArrayList;
public class EraseTest {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("code");
list.add("leader");
//泛型被擦除,按照默认上限Object处理
Object object = list.get(1);
Student s = new Student("张三", 12);
//泛型被擦除,按照第一个上限Number处理
Number score = s.getScore();
}
}
class Student<T extends Number & java.io.Serializable>{
private String name;
private T score;
public Student(String name, T score) {
this.name = name;
this.score = 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;
}
}
4.6 泛型嵌套
当需要为某个泛型形参T指定具体的类型时,如果发现这个具体的类型也是某个泛型类或泛型接口,那么就会出现泛型嵌套的情况。当出现泛型嵌套时,也不用惊慌,只要从外到内,一层一层分析即可。
案例需求:已知有省份Province类型、属性省份编号id和名称name,有城市City类型、属性城市编号id和名称name、所属省份编号pid。如果要存储如下信息到一个Map中,那么如何指定泛型?其中key为省份对象,value为该省份对应的所有城市对象。
案例分析:key的类型为Province,value要保存多个城市对象,因此value是一个List或Set,其泛型实参为City类型,如TreeMap<Province,TreeSet<City>>
。
省份Province类代码:
public class Province implements Comparable<Province> {
private int id; //省份编号
private String name;//省份名称
public Province(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int compareTo(Province o) {
return this.id-o.id;
}
@Override
public String toString() {
return "Province{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
城市City类代码:
public class City implements Comparable<City> {
private int id;//城市编号
private String name;//城市名称
private int pid;//所属省份编号
public City(int id, String name, int pid) {
this.id = id;
this.name = name;
this.pid = pid;
}
@Override
public int compareTo(City o) {
return this.id-o.id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPid() {
return pid;
}
public void setPid(int pid) {
this.pid = pid;
}
@Override
public String toString() {
return "City{" +
"id=" + id +
", name='" + name + '\'' +
", pid=" + pid +
'}';
}
}
测试类代码:
public class AreaManager {
public static void main(String[] args) {
//key为Province,value是一个TreeSet<City>
TreeMap<Province, TreeSet<City>> map = new TreeMap<>();
TreeSet<City> bj = new TreeSet<>();
bj.add(new City(1,"北京市",1));
map.put(new Province(1,"北京市"),bj);
TreeSet<City> hn = new TreeSet<>();
hn.add(new City(1,"海口市",2));
hn.add(new City(2,"三亚市",2));
map.put(new Province(2,"海南省"),hn);
TreeSet<City> zj = new TreeSet<>();
zj.add(new City(1,"绍兴市",3));
zj.add(new City(2,"温州市",3));
zj.add(new City(3,"湖州市",3));
zj.add(new City(4,"嘉兴市",3));
zj.add(new City(5,"台州市",3));
zj.add(new City(6,"金华市",3));
zj.add(new City(7,"舟山市",3));
zj.add(new City(8,"丽水市",3));
map.put(new Province(3,"浙江省"),zj);
//Map中实际存储的是一个个的Entry对象,所有的Entry就组成了一个Set集合
//而Entry类型的key是Province,value是TreeSet<City>
Set<Map.Entry<Province, TreeSet<City>>> entrySet = map.entrySet();
for (Map.Entry<Province, TreeSet<City>> entry : entrySet) {
Province key = entry.getKey();
System.out.println(key);
TreeSet<City> value = entry.getValue();
for (City city : value) {
System.out.println("\t"+city);
}
}
}
}
5、企业面试题
案例需求:List集合中存储了一些英文单词,遍历显示原始顺序;现在要求对集合中的单词按照字母顺序进行排序,不区分大小写,遍历显示排序后的顺序;重新打乱集合中的单词顺序,再遍历显示;最后找出最长的单词。
示例代码:
public class CollectionsTest {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("code");
list.add("leader");
list.add("hello");
list.add("java");
list.add("collections");
list.add("shy");
//遍历显示
System.out.println("排序之前:");
for (String s : list) {
System.out.print(s+",");
}
//排序
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareToIgnoreCase(o2);
}
});
System.out.println("\n排序之后:");
for (String s : list) {
System.out.print(s+",");
}
//打乱顺序
Collections.shuffle(list);
System.out.println("\n打乱顺序之后:");
for (String s : list) {
System.out.print(s+",");
}
String maxLengthStr=Collections.max(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length()-o2.length();
}
});
System.out.println("\n最长的单词是:"+maxLengthStr);
}
}