Java泛型与集合类
走进泛型
为了统计学生成绩,要求设计一个Score对象,包括课程名称、课程号、课程成绩,但是成绩分为两种,一种以优秀、良好、合格来作为结果,还有一种以具体的数字分数作为结果。如何设计这种Score类呢?现在问题在于成绩可能是String类型,也可能是Integer类型,如何才能很好的去存可能出现的两种类型?
此时可以使用Object类声明变量
public class Main {
public class Score{
String name;
String id;
Object score; //因为Object是所有类型的父类,因此既可以存放Integer也能存放String
public Score(String name, String id, Object score){
this.name = name;
this.id = id;
this.score = score;
}
}
}
取值时再进行强制类型转换,但这样无法再编译期确定类型是否安全,项目中代码量非常之大,进行类型比较又会导致额外的开销和增加代码量。
为了解决以上问题,JDK1.5新增了泛型,它能够再编译阶段就检查类型安全,大大提升开发效率。
public class Score<T>{ //将Score转变为泛型类<T>
String name;
String id;
T score; //T为泛型,根据用户提供的类型自动变成对应类型
public Score(String name, String id, T score){
this.name = name;
this.id = id;
this.score = score;
}
}
public class Main {
public static void main(String[] args) {
Score<String> score1 = new Score<String>("数据结构算法与基础", "sjsj99","优秀");
System.out.println(score1.score);
}
}
泛型将数据类型的确定控制在了编译阶段,在编写代码的时候就能明确泛型的类型,如果不符合将无法通过编译。
泛型本质上也是一个语法糖(并不是JVM所支持的语法,编译后会转成编译器支持的语法,比如之前的foreach),编译后会被擦除,便会上面的Object类型调用,但是类型转换由编译器帮我们完成,不是自己进行转换,因此更加安全。
泛型的使用
泛型的定义,实际上就是普通的类多了一个类型参数,也就是在使用时需要指定具体的泛型类型。泛型的名称一般取单个大写字母,比如T代表Type,当然也可以添加数字和其他的字符。
public class Score<T, R>{ //将Score转变为泛型类<T>
String name;
String id;
T score; //T为泛型,根据用户提供的类型自动变成对应类型
R rank;
public Score(String name, String id, T score, R rank){
this.name = name;
this.id = id;
this.score = score;
this.rank = rank;
}
}
在一个普通类型中定义泛型,泛型T称为参数化类型,在定义泛型类的引用时,需要明确指出类型
public class Main {
public static void main(String[] args) {
Score<String, Integer> score1 = new Score<String, Integer>("数据结构算法与基础", "sjsj99","优秀", 2);
System.out.println(score1.score);
System.out.println(score1.rank);
}
}
泛型无法在静态中使用。因为泛型是只有在创建对象后编译器才能明确泛型类型,而静态类型是类所具有的属性,不足以使得编译器完成类型推断。
泛型无法使用基本类型,如果需要基本类型,只能使用基本类型的包装类进行替换(因为泛型本质也是用Object类,基本类型不是继承Object类)。
类的泛型方法
我们只需要把它当作一个未知的类型来使用即可
public class Score<T, R>{ //将Score转变为泛型类<T>
String name;
String id;
T score; //T为泛型,根据用户提供的类型自动变成对应类型
R rank;
public Score(String name, String id, T score, R rank){
this.name = name;
this.id = id;
this.score = score;
this.rank = rank;
}
public T getScore(){ //若方法的返回值类型为泛型,编译器会自动进行推断
return score;
}
public void setScore(T score){ //若方法的形式参数为泛型,那么实参是能是定义时的类型
this.score = score;
}
}
public class Main {
public static void main(String[] args) {
Score<String, Integer> score1 = new Score<String, Integer>("数据结构算法与基础", "sjsj99","优秀", 2);
String i = score1.score;
score1.setScore("良好");
String j = score1.getScore();
System.out.println(j);
}
}
同样地,静态方法无法直接使用类定义的泛型(不是不能使用,是不能直接使用,静态方法可以使用泛型)
自定义泛型方法
由于泛型定义在类上,只有明确具体的类型才能使用,也就是创建对象时完成类型确定,但是静态方法不需要依附于对象,那么只能在使用时再来确定,所以静态方法可以使用泛型,但是需要单独定义:
public static <E> void test(E e){ //在方法定义前声明泛型
System.out.println(e);
}
public class Main {
public static void main(String[] args) {
Score.test("aaa");
Score.test(1);
}
}
同理,成员方法也能自行定义泛型,在实际使用时再进行类型确定
public <E> void test2(E e){
System.out.println(e);
}
public class Main {
public static void main(String[] args) {
Score.test("aaa");
Score.test(1);
Score<String, Integer> score1 = new Score<String, Integer>("XX课程","asdqwe123", "优秀", 9);
score1.test2(1.2);
}
}
其实,无论是泛型类还是泛型方法,再使用时一定要能够进行类型推断,明确类型才行。
注意:一定要区分定义的泛型和方法前定义的泛型。
泛型的引用
可以看到我们在定义一个泛型类的引用时,需要在后面指出类型:
Score<Integer> score; //声明泛型为Integer类型
如果不希望指定类型,或是希望此引用类型可以引用任意泛型的Score类对象,可以使用?通配符,来表示自动匹配任意的可用类型。
由于使用了通配符,编译器就无法进行类型推断了。
泛型的界限
现在有了一个新的需求,没有String类型的成绩了,但是成绩依然可能是整数,小数。这是我们不希望客户将泛型指定为除数字类型外的其他类型,我们就需要使用到泛型的上界定义:
public class Score<T extends Number>{ //设定泛型上街,必须是Number的子类
private final String name;
private final String id;
private T score;
public Score(String name, String id, T score){
this.name = name;
this.id = id;
this.score = score;
}
public T getScore(){
return score;
}
}
public class Main {
public static void main(String[] args) {
Score<Integer> score = new Score<Integer>("Java", "asd123", 90);
System.out.println(score.getScore());
}
}
同样的,泛型通配符也支持泛型的界限
Score<? extends Number> score; //限定为匹配Number及其子类的类型
既然有上界,那么也有下界
Score<? super Integer> score; //限定为匹配Integer及其父类的类型
确定上界后,编译器会自动将类型提升到上限类型。
确定下界后,要使用Object对象变量和方法。
钻石运算符
每次创建泛型对象都需要在前后都表明类型,但实际上后面的类型声明是可以去掉的,因为我们在传入参数时或定义泛型的引用时,就已经明确了类型,因此JDK1.7提供了钻石运算符来简化代码:
public class Main {
public static void main(String[] args) {
Score<Integer> score = new Score<>("Java", "asd123", 90); //<>钻石运算符
System.out.println(score.getScore());
}
}
泛型与多态
泛型不仅仅可以定义在类上,同时也能定义在接口上
public interface ScoreInterface<T> {
T getScore();
void setScore(T t);
}
当实现此接口时,我们可以选择实现类明确泛型类型或是继续使用此泛型,让具体创建的对象来确定类型。
public class Score implements ScoreInterface<String> { //设定泛型上街,必须是Number的子类
private final String name;
private final String id;
public Score(String name, String id){
this.name = name;
this.id = id;
}
@Override
public String getScore() {
return null;
}
@Override
public void setScore(String s) {
}
}
抽象类和接口类似。
多态类型擦除
一个疑问:既然继承后明确了泛型类型,重写的条件是需要和父类的返回类型、形式、参数已知,泛型默认是Object类型,子类明确后变为Number类型,显然不满足重写的条件,但是为什么依然能编译通过呢?
答:实际上是编译器帮助我们生成了两个桥接方法用于支持重写