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类型,显然不满足重写的条件,但是为什么依然能编译通过呢?

答:实际上是编译器帮助我们生成了两个桥接方法用于支持重写