泛型程序设计
在前面我们学习了最重要的类和对象,了解了面向对象编程的思想,注意,非常重要,面向对象是必须要深入理解和掌握的内容,不能草草结束。在本章节,我们还会继续深入了解,从泛型开始,再到数据结构,最后再开始我们的集合类学习,循序渐进。
泛型
为了统计学生成绩,要求设计一个Score对象,包括课程名称、课程号、课程成绩,但是成绩分为两种,一种是以优秀、良好、合格
来作为结果,还有一种就是 60.0、75.5、92.5
这样的数字分数,可能高等数学这门课是以数字成绩进行结算,而计算机网络实验这门课是以等级进行结算,这两种分数类型都有可能出现,那么现在该如何去设计这样的一个Score类呢?
现在的问题就是,成绩可能是String
类型,也可能是Integer
类型,如何才能很好的去存可能出现的两种类型呢?
public class Score {
String name;
String id;
Object value; //因为Object是所有类型的父类,因此既可以存放Integer也能存放String
public Score(String name, String id, Object value) {
this.name = name;
this.id = id;
this.score = value;
}
}
以上的方法虽然很好地解决了多种类型存储问题,但是Object类型在编译阶段并不具有良好的类型判断能力,很容易出现以下的情况:
public static void main(String[] args) {
Score score = new Score("数据结构与算法基础", "EP074512", "优秀"); //是String类型的
...
Integer number = (Integer) score.score; //获取成绩需要进行强制类型转换,虽然并不是一开始的类型,但是编译不会报错
}
使用Object类型作为引用,对于使用者来说,由于是Object类型,所以说并不能直接判断存储的类型到底是String还是Integer,取值只能进行强制类型转换,显然无法在编译期确定类型是否安全,项目中代码量非常之大,进行类型比较又会导致额外的开销和增加代码量,如果不经比较就很容易出现类型转换异常,代码的健壮性有所欠缺
所以说这种解决办法虽然可行,但并不是最好的方案。
为了解决以上问题,JDK 5新增了泛型,它能够在编译阶段就检查类型安全,大大提升开发效率。
泛型类
泛型其实就一个待定类型,我们可以使用一个特殊的名字表示泛型,泛型在定义时并不明确是什么类型,而是需要到使用时才会确定对应的泛型类型。
我们可以将一个类定义为一个泛型类:
public class Score<T> {
//泛型类需要使用<>,我们需要在里面添加1 - N个类型变量
String name;
String id;
T value; //T会根据使用时提供的类型自动变成对应类型
public Score(String name, String id, T value) {
//这里T可以是任何类型,但是一旦确定,那么就不能修改了
this.name = name;
this.id = id;
this.value = value;
}
}
我们来看看这是如何使用的:
public static void main(String[] args) {
Score<String> score = new Score<String>("计算机网络", "EP074512", "优秀");
//因为现在有了类型变量,在使用时同样需要跟上<>并在其中填写明确要使用的类型
//这样我们就可以根据不同的类型进行选择了
String value = score.value; //一旦类型明确,那么泛型就变成对应的类型了
System.out.println(value);
}
泛型将数据类型的确定控制在了编译阶段,在编写代码的时候就能明确泛型的类型,如果类型不符合,将无法通过编译!因为是具体使用对象时才会明确具体类型,所以说静态方法中是不能用的:
只不过这里需要注意一下,我们在方法中使用待确定类型的变量时,因为此时并不明确具体是什么类型,那么默认会认为这个变量是一个Object类型的变量,因为无论具体类型是什么,一定是Object类的子类:
我们可以对其进行强制类型转换,但是实际上没多大必要:
public void test(T t){
String str = (String) t; //都明确要用String了,那这里定义泛型不是多此一举吗
}
因为泛型本身就是对某些待定类型的简单处理,如果都明确要使用什么类型了,那大可不必使用泛型。还有,不能通过这个不确定的类型变量就去直接创建对象和对应的数组:
注意,具体类型不同的泛型类变量,不能使用不同的变量进行接收:
如果要让某个变量支持引用确定了任意类型的泛型,那么可以使用?
通配符:
public static void main(String[] args) {
Test<?> test = new Test<Integer>();
test = new Test<String>();
Object o = test.value; //但是注意,如果使用通配符,那么由于类型不确定,所以说具体类型同样会变成Object
}
当然,泛型变量不止可以只有一个,如果需要使用多个的话,我们也可以定义多个:
public class Test<A, B, C> {
//多个类型变量使用逗号隔开
public A a;
public B b;
public C c;
}
那么在使用时,就需要将这三种类型都进行明确指定:
public static void main(String[] args) {
Test<String, Integer, Character> test = new Test<>(); //使用钻石运算符可以省略其中的类型
test.a = "lbwnb";
test.b = 10;
test.c = '淦';
}
是不是感觉好像还是挺简单的?只要是在类中,都可以使用类型变量:
public class Test<T>{
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
只不过,泛型只能确定为一个引用类型,基本类型是不支持的:
public class Test<T>{
public T value;
}
如果要存放基本数据类型的值,我们只能使用对应的包装类:
public static void main(String[] args) {
Test<Integer> test = new Test<>();
}
当然,如果是基本类型的数组,因为数组本身是引用类型,所以说是可以的:
public static void main(String[] args) {
Test<int[]> test = new Test<>();
}
通过使用泛型,我们就可以将某些不明确的类型在具体使用时再明确。
泛型与多态
不只是类,包括接口、抽象类,都是可以支持泛型的:
public interface Study<T> {
T test();
}
当子类实现此接口时,我们可以选择在实现类明确泛型类型,或是继续使用此泛型让具体创建的对象来确定类型:
public class Main {
public static void main(String[] args) {
A a = new A();
Integer i = a.test();
}
static class A implements Study<Integer> {
//在实现接口或是继承父类时,如果子类是一个普通类,那么可以直接明确对应类型
@Override
public Integer test() {
return null;
}
}
}
或者是继续摆烂,依然使用泛型:
public class Main {
public static void main(String[] args) {
A<String> a = new A<>();
String i = a.test();
}
static class A<T> implements Study<T> {
//让子类继续为一个泛型类,那么可以不用明确
@Override
public T test() {
return null;
}
}
}
继承也是同样的:
static class A<T> {
}
static class B extends A<String> {
}
泛型方法
当然,类型变量并不是只能在泛型类中才可以使用,我们也可以定义泛型方法。
当某个方法(无论是是静态方法还是成员方法)需要接受的参数类型并不确定时,我们也可以使用泛型来表示:
public class Main {
public static void main(String[] args) {
String str = test("Hello World!");
}
private static <T> T test(T t){
//在返回值类型前添加<>并填写泛型变量表示这个是一个泛型方法
return t;
}
}
泛型方法会在使用时自动确定泛型类型,比如上我们定义的是类型T作为参数,同样的类型T作为返回值,实际传入的参数是一个字符串类型的值,那么T就会自动变成String类型,因此返回值也是String类型。
public static void main(String[] args) {
String[] strings = new String[1];
Main main = new Main();
main.add(strings, "Hello");
System.out.println(Arrays.toString(strings));
}
private <T> void add(T[] arr, T t){
arr[0] = t;
}
实际上泛型方法在很多工具类中也有,比如说Arrays的排序方法:
Integer[] arr = {
1, 4, 5, 2, 6, 3, 0, 7, 9, 8};
Arrays.sort(arr, new Comparator<Integer>() {
//通过创建泛型接口的匿名内部类,来自定义排序规则,因为匿名内部类就是接口的实现类,所以说这里就明确了类型
@Override
public int compare(Integer o1, Integer o2) {
//这个方法会在执行排序时被调用(别人来调用我们的实现)
return 0;
}
});
比如现在我们想要让数据从大到小排列,我们就可以自定义:
public static void main(String[] args) {
Integer[] arr = {
1, 4, 5, 2, 6, 3, 0, 7, 9, 8};
Arrays.sort(arr, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//两个需要比较的数会在这里给出
return o2 - o1;
//compare方法要求返回一个int来表示两个数的大小关系,大于0表示大于,小于0表示小于
//这里直接o2-o1就行,如果o2比o1大,那么肯定应该排在前面,所以说返回正数表示大于
}
});
System.out.println(Arrays.toString(arr));
}
因为我们前面学习了Lambda表达式,像这种只有一个方法需要实现的接口,直接安排了:
public static void main(String[] args) {
Integer[] arr = {
1, 4, 5, 2, 6, 3, 0, 7, 9, 8};
Arrays.sort(arr, (o1, o2) -> o2 - o1); //瞬间变一行,效果跟上面是一样的
System.out.println(Arrays.toString(arr));
}
包括数组复制方法:
public static void main(String[] args) {
String[] arr = {
"AAA", "BBB", "CCC"};
String[] newArr = Arrays.copyOf(arr, 3); //这里传入的类型是什么,返回的类型就是什么,也是用到了泛型
System.out.println(Arrays.toString(newArr));
}
因此,泛型实际上在很多情况下都能够极大地方便我们对于程序的代码设计。
泛型的界限
现在有一个新的需求,现在没有String类型的成绩了,但是成绩依然可能是整数,也可能是小数,这时我们不希望用户将泛型指定为除数字类型外的其他类型,我们就需要使用到泛型的上界定义:
public class Score<T extends Number> {
//设定类型参数上界,必须是Number或是Number的子类
private final String name;
private final String id;
private final T value;
public Score(String name, String id, T value) {
this.name = name;
this.id = id;
this.value = value;
}
public T getValue() {
return value;
}
}
只需要在泛型变量的后面添加extends
关键字即可指定上界,使用时,具体类型只能是我们指定的上界类型或是上界类型的子类,不得是其他类型。否则一律报错:
实际上就像这样:
同样的,当我们在使用变量时,泛型通配符也支持泛型的界限:
public static void main(String[] args) {
Score<? extends Integer> score = new Score<>("数据结构与算法", "EP074512", 60);
}
那么既然泛型有上界,那么有没有下界呢?肯定的啊:
只不过下界仅适用于通配符,对于类型变量来说是不支持的。下界限定就像这样:
那么限定了上界后,我们再来使用这个对象的泛型成员,会变成什么类型呢?
public static void main(String[] args) {
Score<? extends Number> score = new Score<>("数据结构与算法基础", "EP074512", 10);
Number o = score.getValue(); //可以看到,此时虽然使用的是通配符,但是不再是Object类型,而是对应的上界
}
但是我们限定下界的话,因为还是有可能是Object,所以说依然是跟之前一样:
public static void main(String[] args) {
Score<? super Number> score = new Score<>("数据结构与算法基础", "EP074512", 10)