1,泛型程序设计
在前面我们学习了最重要的类和对象,了解了面向对象编程的思想,注意,非常重要,面向对象是必须要深入理解和掌握的内容,不能草草结束。在本章节,我们还会继续深入了解,从泛型开始,再到反射,最后再开始我们的集合类学习,循序渐进。
1.1 泛型
为了统计学生成绩,要求设计一个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新增了泛型,它能够在编译阶段就检查类型安全,大大提升开发效率。
1.2 泛型类
泛型其实就一个待定类型,我们可以使用一个特殊的名字表示泛型,泛型在定义时并不明确是什么类型,而是需要到使用时才会确定对应的泛型类型。
我们可以将一个类定义为一个泛型类:
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 = '淦';
}
1.3 泛型与多态
不只是类,包括接口、抽象类,都是可以支持泛型的:
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> {
}
1.4 泛型方法
当然,类型变量并不是只能在泛型类中才可以使用,我们也可以定义泛型方法。
当某个方法(无论是是静态方法还是成员方法)需要接受的参数类型并不确定时,我们也可以使用泛型来表示:
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;
}
});
1.5 泛型的界限
现在有一个新的需求,现在没有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关键字即可指定上界,使用时,具体类型只能是我们指定的上界类型或是上界类型的子类,不得是其他类型。否则一律报错:
1.6 判空包装
Java8还新增了一个非常重要的判空包装类Optional,这个类可以很有效的处理空指针问题。
比如对于下面这样一个很简单的方法:
private static void test(String str){ //传入字符串,如果不是空串,那么就打印长度
if(!str.isEmpty()) {
System.out.println("字符串长度为:"+str.length());
}
}
但是如果我们在传入参数时,丢个null进去,直接原地爆炸:
public static void main(String[] args) {
test(null);
}
private static void test(String str){
if(!str.isEmpty()) { //此时传入的值为null,调用方法马上得到空指针异常
System.out.println("字符串长度为:"+str.length());
}
}
因此我们还需要在使用之前进行判空操作:
private static void test(String str){
if(str == null) return; //这样就可以防止null导致的异常了
if(!str.isEmpty()) {
System.out.println("字符串长度为:"+str.length());
}
}
虽然这种方式很好,但是在Java8之后,有了Optional类,它可以更加优雅地处理这种问题,我们来看看如何使用:
private static void test(String str){
Optional
.ofNullable(str) //将传入的对象包装进Optional中
.ifPresent(s -> System.out.println("字符串长度为:"+s.length()));
//如果不为空,则执行这里的Consumer实现
}
是不是感觉很方便?我们还可以将包装的类型直接转换为另一种类型:
private static void test(String str){
Integer i = Optional
.ofNullable(str)
.map(String::length) //使用map来进行映射,将当前类型转换为其他类型,或者是进行处理
.orElse(-1);
System.out.println(i);
}
当然,Optional的方法比较多,这里就不一一介绍了。