0 引入背景
为了统计学生成绩,要求设计一个Score对象,包括课程名称、课程号、课程成绩,但是成绩分为两种,一种是以优秀、良好、合格
来作为结果,还有一种就是 60.0、75.5、92.5
这样的数字分数,可能高等数学这门课是以数字成绩进行结算,而计算机网络实验这门课是以等级进行结算,这两种分数类型都有可能出现,那么现在该如何去设计这样的一个Score类呢?
现在的问题就是,成绩可能是String
类型,也可能是Integer
类型,如何才能很好的去存可能出现的两种类型呢?
根据以前的顶层Object类知识, 我们可以用 Object value
来存放 Integer
和 String
类来变向实现。
但每次访问时若想使用 Integer
或 String
类的特殊方法, 都需要先进行强制类型转换 (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实现
}