0 引入背景

为了统计学生成绩,要求设计一个Score对象,包括课程名称、课程号、课程成绩,但是成绩分为两种,一种是以优秀、良好、合格 来作为结果,还有一种就是 60.0、75.5、92.5 这样的数字分数,可能高等数学这门课是以数字成绩进行结算,而计算机网络实验这门课是以等级进行结算,这两种分数类型都有可能出现,那么现在该如何去设计这样的一个Score类呢?

现在的问题就是,成绩可能是String类型,也可能是Integer类型,如何才能很好的去存可能出现的两种类型呢?

根据以前的顶层Object类知识, 我们可以用 Object value 来存放 IntegerString 类来变向实现。

但每次访问时若想使用 IntegerString 类的特殊方法, 都需要先进行强制类型转换 (Integer) value 才能使用。过于繁琐。

为了解决这样类似的问题, 同时也是应用JAVA集合类的基础,在 JDK 5 提出了泛型。

1 泛型类

泛型其实就一个待定类型,我们可以使用一个特殊的名字表示泛型,泛型在定义时并不明确是什么类型,而是需要到使用时才会确定对应的泛型类型。

我们可以将一个类定义为一个泛型类:(如果定义在静态方法内部也不行, 因为静态内容是属于类而非对象)

public class Score<T, A> {
	String name;
	String id;
	T value;
	A anovalue; // 另一个泛型
	
	public Score(String name, String id, T value) {
		this.name = name;
		this.value = value;
		this.id = id;
	}
}

接着在主函数中这样使用:

Score<String, Integer> score = new Score<String, Integer>("计算机网络", "EP074512", "优秀")

因为泛型默认是继承自 Object, 因此基本类型是无法接收的, 但可以接受又基本类型构成的数组(其是对象)。

泛型与多态

接口的泛型

public interface Study<T> {
	T test();
}

实现:

public static void main(String[] args) {
	A a = new A();
	Integer i = a.test();
}
static class A implements  Study<Integer> {
	@override
	public T test() {
		return null;
	}
}

如果子类依然是泛型类, 则不需要把接口那里确定类型。

泛型方法

当某个方法(无论静态还是成员方法)需要接受的参数类型不确定时, 也可以用泛型表示:

public static <T>T  test(T t) {
	return t;
}

在返回值类型前加上 <T> 表示这是一个泛型方法。

此时会在使用时自动确定类型, 例如:

String[] strs = new String[20];
main.test(strs)

这里就自动把 T 类型换成了 String。

泛型的界限

泛型对于调用端并不知道应该用那些, 如果只是自己写代码问题不大, 若是两个部门协作,很容易造成对接错误。 为此可以给泛型增加限制, 也就是泛型的界限。

public class Score<T extends Number> {
	...
}

通过添加 extends 指定上界,即在使用时只能是 上界类或其子类。对于例子中就是这样: 也可以指定下界, 但只限于类型适配符:

public static void main(String[] args) {
	Score<? super Object> Score = new Score<Integer>(...);
	// 会报错, 因为 Integer 比 Object 更小
}

这样默认就不会像下界那样为Object而是自己设定的上界:

public static void main(String[] args) {
    Score<? extends Number> score = new Score<>("数据结构与算法基础", "EP074512", 10);
    Number o = score.getValue();   //可以看到,此时虽然使用的是通配符,但是不再是Object类型,而是对应的上界
}
 

类型擦除

JAVA中实现泛型并不是真的具有泛型类型, 而是在编译时先用默认类型(Object)替换,而在应用时自动进行子类强制转换。

public static void main(String[] args) {
    A a = new B();
    String i = (String) a.test("10");   //依靠强制类型转换完成的
}

也因此, 哪怕我们直接不对泛型类型指定, 也是能编译通过(默认类型)。

和语法糖不同的是, JAVA在某些情况, 比如我们重写带有泛型的方法时:

public class B extends A<String>{
    @Override
    String test(String s) {
        return null;
    }
}

按照刚才的说法, 泛型默认就是下界的类型, 那么这里是否违反了方法重写的机制

实际上JAVA会在编译时生成了一个桥接方法用于调用我们重写的方法:

public class B extends A {
    
    public Object test(Object obj) {   //这才是重写的桥接方法
        return this.test((Integer) obj);   //桥接方法调用我们自己写的方法
    }
    
    public String test(String str) {   //我们自己写的方法
        return null;
    }
}

函数式接口

由 JDK 1.8 新增, 用于Lambda表达式的接口。

Supplier 供给型函数式接口

专门用于供给使用的,其中只有一个get方法用于获取需要的对象。

@FunctionalInterface   //函数式接口都会打上这样一个注解
public interface Supplier<T> {
    T get();   //实现此方法,实现供给功能
}

比如我们要实现一个专门供给Student对象Supplier,就可以使用。

public class Student {
    public void hello(){
        System.out.println("我是学生!");
    }
}

应用:

//专门供给Student对象的Supplier
private static final Supplier<Student> STUDENT_SUPPLIER = Student::new;
public static void main(String[] args) {
    Student student = STUDENT_SUPPLIER.get();
    student.hello();
}

Consumer 消费型函数式接口

专门用于消费某个对象。

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);    //这个方法就是用于消费的,没有返回值
 
    default Consumer<T> andThen(Consumer<? super T> after) {   //这个方法便于我们连续使用此消费接口
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

应用:

//专门消费Student对象的Consumer
private static final Consumer<Student> STUDENT_CONSUMER = student -> System.out.println(student+" 真好吃!");
public static void main(String[] args) {
    Student student = new Student();
    STUDENT_CONSUMER.accept(student);
}
 

也可以通过 andThen 来提前构建好消费之后的操作:

public static void main(String[] args) {
    Student student = new Student();
    STUDENT_CONSUMER   //我们可以提前将消费之后的操作以同样的方式预定好
            .andThen(stu -> System.out.println("我是吃完之后的操作!")) 
            .andThen(stu -> System.out.println("好了好了,吃饱了!"))
            .accept(student);   //预定好之后,再执行
}

Function 函数型函数式接口

这个接口消费一个对象,然后会向外供给一个对象(前两个的融合体)

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);   //这里一共有两个类型参数,其中一个是接受的参数类型,还有一个是返回的结果类型
 
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
 
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
 
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

最基本的是 apply 方法,也是我们需要实现的方法。 而 compose 是可以将指定函数式的结果作为当前函数式的实参:

public static void main(String[] args) {
    String str = INTEGER_STRING_FUNCTION
            .compose((String s) -> s.length())   //将此函数式的返回值作为当前实现的实参
            .apply("lbwnb");   //传入上面函数式需要的参数
    System.out.println(str);
}
 

相反 andThen 可以将当前实现的返回值进行进一步的处理, 得到其他类型的值。

Predicate 断言型函数式接口

接受一个参数, 然后进行自定义判断并返回一个布尔类型的结果。

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);    //这个方法就是我们要实现的
 
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }
 
    default Predicate<T> negate() {
        return (t) -> !test(t);
    }
 
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }
 
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

判空包装

Java8还新增了一个非常重要的判空包装类Optional,这个类可以很有效的处理空指针问题。

比如对于下面这样一个很简单的方法:

private static void test(String str){   //传入字符串,如果不是空串,那么就打印长度
    if(!str.isEmpty()) {
        System.out.println("字符串长度为:"+str.length());
    }
}

然而如果传入 null 则就直接报错。通常我们重载 equal 时也会用到, 得在判断前先判断是否为空。

而在 JAVA8 之后,我们就可以用这种方法解决:

private static void test(String str){
    Optional
            .ofNullable(str)   //将传入的对象包装进Optional中
            .ifPresent(s -> System.out.println("字符串长度为:"+s.length()));  
                      //如果不为空,则执行这里的Consumer实现
}