泛型程序设计
为了统计学生成绩,需要设计一个Score
对象,包括课程名称、课程号、课程成绩,但是成绩分为两种,一种是以优秀、良好、及格、不及格来作为结果,另一种是60.0、97.5这样的数字分数,不同的课程可能是用不同的累心,那应该如何设计这样的Score
类。
如果我们使用以下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| package org.ep;
public class Score { String name; String id; Object value; public Score(String name, String id, Object value) { this.name = name; this.id = id; this.value = value; } }
|
以上的方法虽然很好地解决了多种类型存储问题,但是Object类型在编译阶段并不具有良好的类型判断能力,对于使用者来说不是很容易判断存储的类型到底是String
还是Integer
,所以只能在取值的时候进行强制类型转换,又无法保证在编译器的安全性。所以说这种方案不是最好的。
为了解决以上问题,可以使用泛型,能在编译阶段就检查类型安全,提升开发效率。
泛型类
泛型其实就是一个待定类型,我们可以使用一个特殊的名字表示泛型,在使用时不需要定义是什么类型。
我们可以将一个类定义成一个泛型类:
1 2 3 4 5 6 7 8 9 10 11 12 13
| package org.ep;
public class Score<T> { String name; String id; T value;
public Score(String name, String id, T value) { this.name = name; this.id = id; this.value = value; } }
|
使用时需要明确需要使用的类型:
1 2 3 4 5 6 7 8 9
| package org.ep;
public class Main { public static void main(String[] args) { Score<String> score = new Score<String>("csapp", "341", "优秀"); String value = score.value; System.out.println(value); } }
|
泛型将数据类型的确定控制在了编译阶段,在编写代码的时候就能明确泛型的类型,如果类型不符合则不能通过编译。
因为是具体使用对象是才会明确具体类型,所以静态方法中是不能用的。而且不能通过不确定的类型变量直接创建对象和对应的数组。
如果要让某个变量支持引用确定了任意类型的泛型,那么可以用?
通配符:
1 2 3 4 5 6 7 8 9
| package org.ep;
public class Main { public static void main(String[] args) { Test<?> test = new Test<Integer>(); test = new Test<String>(); Object o = test.value; } }
|
这样,我们就创建了一个Test
泛型类的test
对象,这个对象可以分配给不同类型的Test
对象,比如Test<Integer>
或 Test<String>
,因为通配符 ?
表示不确定的泛型类型。但是注意,如果使用通配符,那么由于类型不确定,所以说具体类型同样会变成Object
。
当然,泛型变量不止可以只有一个,如果需要使用多个的话,我们也可以定义多个:
1 2 3 4 5 6 7
| package org.ep;
public class Test<A, B, C> { A a; B b; C c; }
|
这样在使用时就需要将三种类型都明确指定:
1 2 3 4 5 6 7 8 9 10
| package org.ep;
public class Main { public static void main(String[] args) { Test<String, Integer, Character> test = new Test<>(); test.a = "EP"; test.b = 13; test.c = 't'; } }
|
只要在类中,都可以使用类型变量:
1 2 3 4 5 6 7 8 9 10 11 12 13
| package org.ep;
public class Test<T> { private T value; public void setValue(T value) { this.value = value; } public T getValue() { return value; } }
|
只不过,泛型只能确定为一个引用类型,基本类型(int、double等)是不支持的。如果要存放基本数据类型的值,我们只能使用对应的包装类:
1 2 3 4 5 6 7
| package org.ep;
public class Main { public static void main(String[] args) { Test<Integer> test = new Test<>(); } }
|
但是因为基本类型的数组时引用类型,所以可以直接使用:
1 2 3 4 5 6 7
| package org.ep;
public class Main { public static void main(String[] args) { Test<int[]> test = new Test<>(); } }
|
通过使用泛型,我们就可以将某些不明确的类型在具体使用时再明确。
泛型与多态
不只是类,包括接口、抽象类,都支持泛型:
1 2 3 4 5
| package org.ep;
public interface Study<T> { T test(); }
|
当子类实现此接口时,我们可以选择在实现类明确泛型类型,或是继续使用此泛型让具体创建的对象来确定类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package org.ep;
public class Main { public static void main(String[] args) { A a = new A(); Integer i = a.test(); }
public static class A implements Study<Integer> { public Integer test() { return null; } } }
|
继续使用泛型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package org.ep;
public class Main { public static void main(String[] args) { A<String> a = new A(); String i = a.test(); }
public static class A<T> implements Study<T> { public T test() { return null; } } }
|
继承也是相同的:
1 2 3 4 5 6 7
| static class A<T> { }
static class B extends A<String> {
}
|
泛型方法
类型变量不只有在泛型类中才能使用,也可以定义泛型方法。
当某个方法(无论是是静态方法还是成员方法)需要接受的参数类型并不确定时,我们也可以使用泛型来表示:
1 2 3 4 5 6 7 8 9 10 11
| package org.ep;
public class Main { public static void main(String[] args) { System.out.println(test("enen")); }
private static <T> T test(T t) { return t; } }
|
泛型方法会在使用时自动确定泛型类型,比如上我们定义的是类型T作为参数,同样的类型T作为返回值,实际传入的参数是一个字符串类型的值,那么T就会自动变成String类型,因此返回值也是String类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package org.ep;
import java.util.Arrays;
public class Main { 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 static <T> void add(T[] arr, T t) { arr[0] = t; } }
|
实际上泛型方法在很多工具类中也有,比如说Arrays
的排序方法:
1 2 3 4 5 6 7 8 9 10 11 12
| 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; } }); System.out.println(Arrays.toString(arr)); }
|
Arrays.sort()
方法可以接受一个比较器(Comparator)作为参数,这是因为 Java
提供了方法重载(Overloading)的机制,允许同一个方法接受不同类型或数量的参数。在 Arrays
类中,有多个重载的 sort
方法,其中一些接受不同类型的比较器。
Arrays.sort(T[] a, Comparator<? super T> c)
方法接受一个泛型数组 a 和一个比较器 c,用于对数组进行排序。这个方法的定义允许通过传递一个自定义的比较器来指定排序规则。
比较器是一种实现了 Comparator
接口的类,该接口定义了一个 compare
方法,用于比较两个对象的顺序。你可以通过实现 compare
方法来自定义排序规则,然后将这个比较器对象传递给 Arrays.sort()
方法。
根据前面学习的Lambda
表达式,这种只有一个方法需要实现的接口,就可以使用Lambda
表达式实现Comparator
接口:
1 2 3 4 5 6 7 8 9 10 11
| package org.ep;
import java.util.Arrays;
public class Main { 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)); } }
|
1
| [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
|
因此,泛型实际上在很多情况下都能够极大地方便我们对于程序的代码设计
泛型的界限
现在有一个新的需求,现在没有String类型的成绩了,但是成绩依然可能是整数,也可能是小数,这时我们不希望用户将泛型指定为除数字类型外的其他类型,我们就需要使用到泛型的上界定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package org.ep;
public class Score<T extends 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 getScore() { return value; } }
|
只要在泛型变量后面添加extends
关键字就可以指定上界,使用时,具体类型只能是我们指定的上界类型或是上界类型的子类,不得是其他类型。否则一律报错:
同样的,当我们在使用变量时,泛型通配符也支持泛型的界限:
1 2 3 4 5 6 7 8 9
| package org.ep;
import java.util.Arrays;
public class Main { public static void main(String[] args) { Score<? extends Integer> score = new Score<>("E", "13", 60); } }
|
同样的,泛型也有下界:
只不过下界仅适用于通配符,对于类型变量来说是不支持的。
下界的限定就像这样:
限定了上界后,再来使用这个对象的泛型成员,会变成什么类型呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package org.ep;
public class Score<T> { 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 getScore() { return value; } }
|
1 2 3 4 5 6 7 8 9 10
| package org.ep;
import java.util.Arrays;
public class Main { public static void main(String[] args) { Score<? extends Number> score = new Score<>("E", "13", 60); Number n = score.getScore(); } }
|
也就是说此时虽然使用了通配符,但是因为限定了上界,所以泛型变量已经不再是Object
类型,而是变成了上界Number
类型。但是如果限定下界,就不会改变泛型变量的类型,保持Object
不变。
通过给设定泛型上限,我们就可以更加灵活地控制泛型的具体类型范围。
类型擦除
类型擦除是一种编译期优化,旨在确保泛型代码与旧版非泛型代码兼容,并且在运行时减少了泛型信息的存储开销。泛型的类型擦除主要包括以下几个方面:
- 类型参数擦除:在编译时,泛型类型的类型参数会被擦除,例如,
List<Integer>
和 List<String>
在运行时都被视为 List
- 泛型方法擦除:泛型方法的类型参数也会被擦除,方法的参数和返回类型会被转换为适当的泛型类型。
- 泛型类擦除:泛型类的类型参数也会被擦除,类的字段和方法会根据泛型类型参数的擦除后的类型进行调整。
- 桥方法生成:当泛型类或接口涉及到继承或实现时,编译器会生成桥方法,以确保类型安全性。
前面已经了解了如何使用泛型,那么泛型到底是怎么实现的:
1 2 3 4 5
| package org.ep;
public abstract class A<T> { abstract T test (T t); }
|
泛型类型并不是一个真正存在的类型,因为所有的对象都是属于一个普通的类型,一个泛型类型变异之后,实际上会直接使用默认的类型Object
:
1 2 3 4 5
| package org.ep;
public abstract class A { abstract Object test (Object t); }
|
如果我们限定了上界,则默认类型就会变成我们限定的上界的类型:
1 2 3 4 5
| package org.ep;
public abstract class A<T extends Number> { abstract T test (T t); }
|
那么编译之后:
1 2 3 4 5
| package org.ep;
public abstract class A { abstract Number test (Number t); }
|
因此,泛型类型仅仅是在编译阶段进行类型检查,当程序在运行时并不会检查对应类型,所以说哪怕是我们不去指定类型也可以直接使用:
1 2 3 4 5 6 7
| package org.ep;
public class Main { public static void main(String[] args) { Test test = new Test(); } }
|
只不过此时编译器会给出警告:
同样的,由于类型擦除,实际上我们在使用时,编译后的代码是进行了强制类型转换的:
1 2 3 4 5 6 7 8
| package org.ep;
public class Main { public static void main(String[] args) { A<String> a = new B(); String i = a.test("10"); } }
|
实际上编译之后:
1 2 3 4
| public static void main(String[] args) { A a = new B(); String i = (String) a.test("10"); }
|
思考一个问题:继承泛型类之后可以明确具体类型,那么为什么@Override
不会出现错误呢?前面提到过,重写的条件是需要和父类的返回值类型和形参一致,而泛型默认的原始类型是Object
类型,子类明确后变为其他类型,明显不满足重写的条件,为什么不会出错?
1 2 3 4 5 6 7 8
| package org.ep;
public class B extends A<String>{ @Override String test(String a) { return null; } }
|
实际上编译之后:
1 2 3 4 5 6
| public class com.test.entity.B extends com.test.entity.A<java.lang.String> { public com.test.entity.B(); java.lang.String test(java.lang.String); java.lang.Object test(java.lang.Object); }
|
通过反编译进行观察,实际上是编译器帮助我们生成了一个桥接方法用于支持重写:
1 2 3 4 5 6 7 8 9 10
| public class B extends A { public Object test(Object obj) { return this.test((Integer) obj); } public String test(String str) { return null; } }
|
类型擦除机制其实就是为了方便使用后面集合类(不然每次都要强制类型转换)同时为了向下兼容采取的方案。因此,泛型的使用会有一些限制:
首先,在进行类型判断时instanceof Test<String
,不允许使用泛型,只能使用原始类型,其次,泛型类型是不支持创建参数化类型数组的:Test<String>[] test = new Test<String>[10]
,因为运行时不会检查具体类型是什么。
函数式接口
函数式接口就是专门用于Lambda
表达式的接口,这些接口都可以直接使用Lambda
表达式,非常方便。
Supplier
供给型函数式接口
这个接口时专门用于供给使用的,其中只有一个get
方法用于获取需要的对象。
1 2 3 4
| @FunctionalInterface public interface Supplier<T> { T get(); }
|
比如我们想实现一个专门供给Student
对象的Supplier
就可以使用:
1 2 3 4 5 6 7 8
| package org.ep;
public class Student { public void hello() { System.out.println("hi"); } }
|
1 2 3 4 5 6 7 8 9 10 11
| package org.ep;
import java.util.function.Supplier;
public class Main { private static final Supplier<Student> STUDENT_SUPPLIER = Student::new; public static void main(String[] args) { Student student = STUDENT_SUPPLIER.get(); student.hello(); } }
|
在这段代码中,创建了一个类型为Supplier<Student>
类型的常量STUDENT_SUPPLIER
,这个常量通过方法引用Student::new
来表示一个能提供Student
类对象的Supplier
。然后在main
方法中,使用STUDENT_SUPPLIER.get()
调用了这个Supplier
获取了一个Student
对象,然后调用了hello()
方法来执行Student
对象的hello
方法。
通常情况下,对象的创建是一个开销较大的操作,如果你在某些情况下不需要立即创建对象,可以使用延迟创建的方式,只有在需要时才实际创建对象。使用这种方法就可以实现延迟创建对象,可以提高程序的性能和效率,因为不会在不需要的时候创建对象,从而节省了资源。
Consumer
消费型函数式接口
专门用于消费某个对象:
1 2 3 4 5 6 7 8 9
| @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); }; } }
|
Consumer
代表了接受一个输入参数并且不返回任何结果的操作。它的作用是执行一些操作或者消费输入的数据,通常用于需要对数据进行处理、输出、或者其他副作用操作的情况。Consumer
接口定义了一个名为 accept
的抽象方法,该方法接受一个参数并执行操作。这个接口可以用于各种场景,比如集合操作中对每个元素执行相同的操作,文件处理中对每行文本执行操作等等。
1 2 3 4 5 6
| private static final Consumer<Student> STUDENT_CONSUMER = student -> System.out.println(student+" eating"); public static void main(String[] args) { Student student = new Student(); STUDENT_CONSUMER.accept(student); }
|
也可以使用andThen
方法继续调用:
1 2 3 4 5 6 7
| public static void main(String[] args) { Student student = new Student(); STUDENT_CONSUMER .andThen(stu -> System.out.println("after eating")) .andThen(stu -> System.out.println("full")) .accept(student); }
|
Function
函数型函数式接口
这个接口消费一个对象,然后会向外供给一个对象(前两个的融合体)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @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
方法:
1 2 3 4 5 6
| private static final Function<Integer, String> INTEGER_STRING_FUNCTION = Object::toString; public static void main(String[] args) { String str = INTEGER_STRING_FUNCTION.apply(10); System.out.println(str); }
|
我们可以使用compose
将指定函数式的结果作为当前函数式的实参:
1 2 3 4 5 6
| public static void main(String[] args) { String str = INTEGER_STRING_FUNCTION .compose((String s) -> s.length()) .apply("lbwnb"); System.out.println(str); }
|
相反的,andThen
可以将当前实现的返回值进行进一步的处理,得到其他类型的值:
1 2 3 4 5 6
| public static void main(String[] args) { Boolean str = INTEGER_STRING_FUNCTION .andThen(String::isEmpty) .apply(10); System.out.println(str); }
|
Predicate
断言型函数式接口
接收一个参数,然后进行自定义判断并返回一个boolean结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @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); } }
|
编写一个简单的例子:
1 2 3
| public class Student { public int score; }
|
1 2 3 4 5 6 7 8 9 10
| private static final Predicate<Student> STUDENT_PREDICATE = student -> student.score >= 60; public static void main(String[] args) { Student student = new Student(); student.score = 80; if(STUDENT_PREDICATE.test(student)) { System.out.println("及格"); } else { System.out.println("不及格"); } }
|
也可以使用组合条件判断:
1 2 3 4 5 6 7 8
| public static void main(String[] args) { Student student = new Student(); student.score = 80; boolean b = STUDENT_PREDICATE .and(stu -> stu.score > 90) .test(student); if(!b) System.out.println("优秀"); }
|
同样的,这个类型提供了一个对应的实现,用于判断两个对象是否相等:
1 2 3 4
| public static void main(String[] args) { Predicate<String> predicate = Predicate.isEqual("Hello World"); System.out.println(predicate.test("Hello World")); }
|
通过使用这四个核心的函数式接口,我们就可以使得代码更加简洁。使用场景在后面再细化。
判空包装
Optional
类可以很有效的处理空指针问题。
比如对于下面这样一个很简单的方法:
1 2 3 4 5
| private static void test(String str){ if(!str.isEmpty()) { System.out.println("length is:"+str.length()); } }
|
但是如果传入一个null
,就会出现错误。所以我们在使用之前进行判空操作:
1 2 3 4 5 6
| private static void test(String str){ if(str == null) return; if(!str.isEmpty()) { System.out.println("length is:"+str.length()); } }
|
以上的这种情况,有了Optional
类,就可以更加简便地处理这种问题:
1 2 3 4 5 6
| private static void test(String str){ Optional .ofNullable(str) .ifPresent(s -> System.out.println("length is:"+s.length())); }
|
并且包装之后,再获取时就可以再简便一点:
1 2 3 4
| private static void test(String str){ String s = Optional.ofNullable(str).get(); System.out.println(s); }
|
我们可以对于这种有可能为空的情况进行处理,如果为空,那么就返回另一个备选方案:
1 2 3 4
| private static void test(String str){ String s = Optional.ofNullable(str).orElse("Optional"); System.out.println(s); }
|
我们还可以将包装的类型直接转换为另一种类型:
1 2 3 4 5 6 7
| private static void test(String str){ Integer i = Optional .ofNullable(str) .map(String::length) .orElse(-1); System.out.println(i); }
|