java学习笔记之基础:反射、注解、泛型
反射
反射 Reflection 是指程序在运行期可以拿到一个对象的所有信息。反射是为了解决在运行期对某个实例一无所知的情况下如何调用其方法。
Class 类
class 是由 JVM 在执行过程中动态加载的。JVM 在第一次读取到一种 class 类型时将其加载进内存。每加载一种 class,JVM 就为其创建一个 Class 类型的实例并关联起来。注意:这里的 Class 类型是一个名叫 Class 的 class。它长这样:
public final class Class {
private Class() {}
}
以 String 类为例,当 JVM 加载 String 类时,它首先读取 String.class 文件到内存,然后为 String 类创建一个 Class 实例并关联起来:Class cls = new Class(String); 。这个 Class 实例是 JVM 内部创建的,如果我们查看 JDK 源码,可以发现 Class 类的构造方法是 private,只有 JVM 能创建 Class 实例,我们自己的 Java 程序是无法创建 Class 实例的。所以 JVM 持有的每个 Class 实例都指向一个数据类型(class 或 interface)。
由于 JVM 为每个加载的 class 创建了对应的 Class 实例,并在实例中保存了该 class 的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此如果获取了某个 Class 实例,我们就可以通过这个 Class 实例获取到该实例对应的 class 的所有信息。这种通过 Class 实例获取 class 信息的方法称为反射 Reflection 。
获取一个 class 的 Class 实例:
// 方法一:直接通过一个class的静态变量class获取:
Class cls = String.class;
// 方法二:如果我们有一个实例变量,可以通过该实例变量提供的 getClass() 方法获取:
String s = "Hello";
Class cls = s.getClass();
// 方法三:如果知道一个class的完整类名,可以通过静态方法 Class.forName() 获取:
Class cls = Class.forName("java.lang.String");
因为 Class 实例在 JVM 中是唯一的,所以上述方法获取的 Class 实例是同一个实例。可以用 == 比较两个 Class 实例。通常情况下我们应该用 instanceof 判断数据类型。只有在需要精确判断一个类型是不是某个 class 的时候,我们才使用 == 判断 class 实例。
Class cls = s.getClass();
System.out.println("Class name: " + cls.getName());
System.out.println("Simple name: " + cls.getSimpleName());
if (cls.getPackage() != null) {
System.out.println("Package name: " + cls.getPackage().getName());
}
System.out.println("is interface: " + cls.isInterface());
System.out.println("is enum: " + cls.isEnum());
System.out.println("is array: " + cls.isArray());
System.out.println("is primitive: " + cls.isPrimitive());
动态加载
JVM 在执行 Java 程序的时候,并不是一次性把所有用到的 class 全部加载到内存,而是第一次需要用到 class 时才加载。
// Main.java
public class Main {
public static void main(String[] args) {
if (args.length > 0) {
create(args[0]);
}
}
static void create(String name) {
Person p = new Person(name);
}
}
当执行 Main.java 时,JVM 首先会把 Main.class 加载到内存,然而并不会加载 Person.class ,除非程序执行到 create() 方法,JVM 发现需要加载 Person 类时,才会首次加载 Person.class。如果没有执行 create() 方法,那么 Person.class 根本就不会被加载。这就是 JVM 动态加载 class 的特性。
动态加载 class 的特性对于 Java 程序非常重要。利用 JVM 动态加载 class 的特性,我们才能在运行期根据条件加载不同的实现类。例如 Commons Logging 总是优先使用 Log4j,只有当 Log4j 不存在时,才使用 JDK 的 logging 。利用 JVM 动态加载特性,大致的实现代码如下:
// Commons Logging 优先使用 Log4j:
LogFactory factory = null;
if (isClassPresent("org.apache.logging.log4j.Logger")) {
factory = createLog4j();
} else {
factory = createJdkLog();
}
boolean isClassPresent(String name) {
try {
Class.forName(name);
return true;
} catch (Exception e) {
return false;
}
}
这就是为什么我们只需要把 Log4j 的 jar 包放到 classpath 中,Commons Logging 就会自动使用 Log4j 的原因。
访问字段
Class 类提供了以下几个方法来获取字段:
Field getField(name):根据字段名获取某个 public 的 field(包括父类)Field getDeclaredField(name):根据字段名获取当前类的某个 field(不包括父类)Field[] getFields():获取所有 public 的 field(包括父类)Field[] getDeclaredFields():获取当前类的所有 field(不包括父类)
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public字段"score":
System.out.println(stdClass.getField("score"));
// 获取继承的public字段"name":
System.out.println(stdClass.getField("name"));
// 获取private字段"grade":
System.out.println(stdClass.getDeclaredField("grade"));
}
}
class Student extends Person {
public int score;
private int grade;
}
class Person {
public String name;
}
上述代码分别获取 public 字段、继承的 public 字段以及 private 字段,打印出的 Field 对象类似:
public int Student.score
public java.lang.String Person.name
private int Student.grade
一个 Field 对象包含了一个字段的所有信息:
getName():返回字段名称,例如 "name";getType():返回字段类型,也是一个 Class 实例,例如 String.class;getModifiers():返回字段的修饰符,它是一个 int,不同的 bit 表示不同的含义。
获取字段定义信息
Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false
获取字段值
Object p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true);
Object value = f.get(p);
System.out.println(value); // "Xiao Ming"
反射是一种非常规的用法。使用反射代码非常繁琐,其次它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。此外setAccessible(true) 可能会失败。如果 JVM 运行期存在 SecurityManager,那么它会根据规则进行检查,有可能阻止 setAccessible(true) 。例如某个 SecurityManager 可能不允许对 java 和 javax 开头的 package 的类调用 setAccessible(true) ,这样可以保证 JVM 核心库的安全。
设置字段值
Person p = new Person("Xiao Ming");
System.out.println(p.getName()); // "Xiao Ming"
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true);
f.set(p, "Xiao Hong");
System.out.println(p.getName()); // "Xiao Hong"
调用方法
Class 类提供了以下几个方法来获取 Method:
Method getMethod(name, Class...):获取某个 public 的 Method(包括父类)Method getDeclaredMethod(name, Class...):获取当前类的某个 Method(不包括父类)Method[] getMethods():获取所有 public 的 Method(包括父类)Method[] getDeclaredMethods():获取当前类的所有 Method(不包括父类)
// reflection
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public方法getScore,参数为String:
System.out.println(stdClass.getMethod("getScore", String.class));
// 获取继承的public方法getName,无参数:
System.out.println(stdClass.getMethod("getName"));
// 获取private方法getGrade,参数为int:
System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
}
}
class Student extends Person {
public int getScore(String type) {
return 99;
}
private int getGrade(int year) {
return 1;
}
}
class Person {
public String getName() {
return "Person";
}
}
上述代码分别获取 public 方法、继承的 public 方法以及 private 方法,打印出的 Method 类似:
public int Student.getScore(java.lang.String)
public java.lang.String Person.getName()
private int Student.getGrade(int)
一个 Method 对象包含一个方法的所有信息:
getName():返回方法名称,例如:"getScore";getReturnType():返回方法返回值类型,也是一个 Class 实例,例如:String.class;getParameterTypes():返回方法的参数类型,是一个 Class 数组,例如:{String.class, int.class};getModifiers():返回方法的修饰符,它是一个 int,不同的 bit 表示不同的含义。
当我们获取到一个 Method 对象时,就可以对它进行调用。
// reflection
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
// String对象:
String s = "Hello world";
// 获取String substring(int)方法,参数为int:
Method m = String.class.getMethod("substring", int.class);
// 在s对象上调用该方法并获取结果:
String r = (String) m.invoke(s, 6);
// 打印调用结果:
System.out.println(r); // "world"
}
}
对 Method 实例调用 invoke 就相当于调用该方法,invoke 的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。
调用静态方法时,由于无需指定实例对象,所以 invoke 方法传入的第一个参数永远为 null。
// 获取Integer.parseInt(String)方法,参数为String:
Method m = Integer.class.getMethod("parseInt", String.class);
// 调用该静态方法并获取结果:
Integer n = (Integer) m.invoke(null, "12345");
// 打印调用结果:
System.out.println(n);
和 Field 类似,对于非 public 方法,我们虽然可以通过 Class.getDeclaredMethod() 获取该方法实例,但直接对其调用将得到一个 IllegalAccessException。为了调用非 public 方法,我们通过 Method.setAccessible(true) 允许其调用。setAccessible(true) 可能会失败。
// reflection
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person();
Method m = p.getClass().getDeclaredMethod("setName", String.class);
m.setAccessible(true);
m.invoke(p, "Bob");
System.out.println(p.name);
}
}
class Person {
String name;
private void setName(String name) {
this.name = name;
}
}
调用构造方法
我们通常使用 new 操作符创建新的实例:Person p = new Person();。如果通过反射来创建新的实例,可以调用 Class 提供的 newInstance()方法:Person p = Person.class.newInstance();。调用 Class.newInstance() 的局限是,它只能调用该类的 public 无参数构造方法。如果构造方法带有参数或者不是 public,就无法直接通过 Class.newInstance() 来调用。为了调用任意的构造方法,Java 的反射 API 提供了 Constructor 对象,它包含一个构造方法的所有信息,可以创建一个实例。
import java.lang.reflect.Constructor;
public class Main {
public static void main(String[] args) throws Exception {
// 获取构造方法Integer(int):
Constructor cons1 = Integer.class.getConstructor(int.class);
// 调用构造方法:
Integer n1 = (Integer) cons1.newInstance(123);
System.out.println(n1);
// 获取构造方法Integer(String)
Constructor cons2 = Integer.class.getConstructor(String.class);
Integer n2 = (Integer) cons2.newInstance("456");
System.out.println(n2);
}
}
通过 Class 实例获取 Constructor 的方法如下:
getConstructor(Class...):获取某个 public 的 Constructor;getDeclaredConstructor(Class...):获取某个 Constructor;getConstructors():获取所有 public 的 Constructor;getDeclaredConstructors():获取所有 Constructor。
注意 Constructor 总是当前类定义的构造方法和父类无关,因此不存在多态的问题。调用非 public 的 Constructor 时,必须首先通过 setAccessible(true) 设置允许访问。setAccessible(true) 可能会失败。
获取继承关系
Class i = Integer.class;
Class n = i.getSuperclass();
System.out.println(n);
Class o = n.getSuperclass();
System.out.println(o);
System.out.println(o.getSuperclass());
当我们获取到某个 Class 对象时,实际上就获取到了一个类的类型。可以看到 Integer 的父类类型是 Number,Number 的父类是 Object,Object 的父类是 null。除 Object 外,其他任何非 interface 的 Class 都必定存在一个父类类型。
当我们判断一个实例是否是某个类型时,使用 instanceof 操作符:
Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true
如果是两个 Class 实例,要判断一个向上转型是否成立,可以调用 isAssignableFrom() :
Integer.class.isAssignableFrom(Integer.class); // true,因为 Integer 可以赋值给 Integer
Number.class.isAssignableFrom(Integer.class); // true,因为 Integer 可以赋值给 Number
Object.class.isAssignableFrom(Integer.class); // true,因为 Integer 可以赋值给 Object
Integer.class.isAssignableFrom(Number.class); // false,因为 Number 不能赋值给 Integer
获取 interface
由于一个类可能实现一个或多个接口,通过 Class 我们就可以查询到实现的接口类型。
Class s = Integer.class;
Class[] is = s.getInterfaces();
for (Class i : is) {
System.out.println(i);
}
运行上述代码可知,Integer 实现的接口有:java.lang.Comparable、java.lang.constant.Constable、java.lang.constant.ConstantDesc。要特别注意:getInterfaces() 只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型。如果一个类没有实现任何 interface,那么 getInterfaces() 返回空数组。
动态代理
interface 类型的变量总是通过某个实例向上转型并赋值给接口类型变量的。Java 标准库提供了一种动态代理的机制,可以在运行期动态创建某个 interface 的实例。JDK 提供的动态创建接口对象的方式,就叫动态代理。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
// 定义一个 InvocationHandler 实例,负责实现接口的方法调用
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
// 通过 Proxy.newProxyInstance() 创建 interface 实例,将返回的 Object 强制转型为接口
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}
interface Hello {
void morning(String name);
}
动态代理实际上是 JVM 在运行期动态创建 class 字节码并加载的过程。
注解
注解是放在 Java 源码的类、方法、字段、参数前的一种标注。注解可以被编译器打包进入 class 文件,因此注解是一种用作标注的“元数据”。
@Resource("hello")
public class Hello {
@Inject
int n;
@PostConstruct
public void hello(@Param String name) {
System.out.println(name);
}
@Override
public String toString() {
return "Hello";
}
}
注解的作用
从 JVM 的角度看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。
Java 的注解可以分为三类:
- 第一类是由编译器使用的注解,例如
@Override作用让编译器检查该方法是否正确地实现了覆写;@SuppressWarnings作用告诉编译器忽略此处代码产生的警告。这类注解不会被编译进入.class文件,它们在编译后就被编译器扔掉了。 - 第二类是由工具处理
.class文件使用的注解,比如有些工具会在加载 class 的时候,对 class 做动态修改,实现一些特殊的功能。这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。 - 第三类是在程序运行期能够读取的注解,它们在加载后一直存在于 JVM 中,这也是最常用的注解。例如一个配置了
@PostConstruct的方法会在调用构造方法后自动被调用(这是 Java 代码读取该注解实现的功能,JVM 并不会识别该注解)。
定义一个注解时还可以定义配置参数。配置参数可以包括所有基本类型、String、枚举类型、[基本类型|String|Class|枚举]的数组。配置参数必须是常量,此限制保证了注解在定义时就已经确定了每个参数的值。注解的配置参数可以有默认值,缺少某个配置参数时将使用默认值。
此外大部分注解会有一个名为 value 的配置参数,对此参数赋值可以只写常量,相当于省略了 value 参数。如果只写注解,相当于全部使用默认值。
public class Hello {
@Check(min=0, max=100, value=55)
public int n;
@Check(value=99)
public int p;
@Check(99) // @Check(value=99)
public int x;
@Check // 表示所有参数都使用默认值
public int y;
}
元注解
有一些注解可以修饰其他注解,这些注解就称为元注解。Java 标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。
@Target
最常用的元注解是 @Target。使用 @Target 可以定义注解能够被应用于源码的哪些位置:
- 类或接口:ElementType.TYPE
- 字段:ElementType.FIELD
- 方法:ElementType.METHOD
- 构造方法:ElementType.CONSTRUCTOR
- 方法参数:ElementType.PARAMETER
例如定义注解 @Report 可用在方法上,我们必须添加一个 @Target(ElementType.METHOD):
@Target(ElementType.METHOD)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
定义注解 @Report 可用在方法或字段上,可以把 @Target 注解参数变为数组{ ElementType.METHOD, ElementType.FIELD }:
@Target({
ElementType.METHOD,
ElementType.FIELD
})
public @interface Report {
...
}
@Retention
元注解 @Retention 定义了注解的生命周期:
- 仅编译期:RetentionPolicy.SOURCE
- 仅 class 文件:RetentionPolicy.CLASS
- 运行期:RetentionPolicy.RUNTIME
如果 @Retention 不存在,则该 Annotation 默认为 CLASS。因为通常我们自定义的 Annotation 都是 RUNTIME,所以务必要加上@Retention(RetentionPolicy.RUNTIME) 这个元注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
@Repeatable
使用 @Repeatable 这个元注解可以定义注解是否可重复。
@Repeatable(Reports.class)
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
@Target(ElementType.TYPE)
public @interface Reports {
Report[] value();
}
经过 @Repeatable 修饰后,在某个类型声明处就可以添加多个 @Report 注解:
@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}
@Inherited
使用 @Inherited 定义子类是否可继承父类定义的注解。@Inherited 仅针对 @Target(ElementType.TYPE)类型的注解有效并且仅针对 class 的继承,对 interface 的继承无效:
@Inherited
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
在使用的时候,如果一个类用到了 @Report,则它的子类默认也定义了该注解。
@Report(type=1)
public class Person {
}
定义注解
Java 语言使用 @interface 语法来定义注解。注解的参数类似无参数方法,可以用 default 设定一个默认值(强烈推荐)。最常用的参数应当命名为 value。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
步骤:用 @interface 定义注解,添加参数、默认值,用元注解配置注解。
其中必须设置 @Target 和 @Retention,@Retention 一般设置为 RUNTIME,因为我们自定义的注解通常要求在运行期读取。一般情况下不必写 @Inherited 和 @Repeatable。
处理注解
Java 的注解本身对代码逻辑没有任何影响。如何使用注解完全由工具决定。
Java 提供的使用反射 API 读取注解的方法包括判断某个注解是否存在于 Class、Field、Method 或 Constructor 的方法:Class.isAnnotationPresent(Class)、Field.isAnnotationPresent(Class)、Method.isAnnotationPresent(Class)、Constructor.isAnnotationPresent(Class)。使用反射 API 读取注解的方法:Class.getAnnotation(Class)、Field.getAnnotation(Class)、Method.getAnnotation(Class)、Constructor.getAnnotation(Class)。
使用反射 API 读取注解有两种方法。方法一是先判断注解是否存在,如果存在,就直接读取:
Class cls = Person.class;
if (cls.isAnnotationPresent(Report.class)) {
Report report = cls.getAnnotation(Report.class);
int type = report.type();
String level = report.level();
}
第二种方法是直接读取注解,如果注解不存在,将返回 null:
Class cls = Person.class;
Report report = cls.getAnnotation(Report.class);
if (report != null) {
...
}
读取方法、字段和构造方法的注解和 Class 类似。但要读取方法参数的注解就比较麻烦一点,因为方法参数本身可以看成一个数组,而每个参数又可以定义多个注解,所以一次获取方法参数的所有注解就必须用一个二维数组来表示。要读取方法参数的注解,我们先用反射获取 Method 实例,然后读取方法参数的所有注解:
// 获取Method实例:
Method m = ...
// 获取所有参数的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一个参数(索引为0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
if (anno instanceof Range r) { // @Range注解
r.max();
}
if (anno instanceof NotNull n) { // @NotNull注解
//
}
}
使用注解
注解如何使用,完全由程序自己决定。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
int min() default 0;
int max() default 255;
}
在某个 JavaBean 中,我们可以使用该注解:
public class Person {
@Range(min=1, max=20)
public String name;
@Range(max=10)
public String city;
}
定义了注解对程序逻辑没有任何影响。我们必须自己编写代码来使用注解。我们编写一个 Person 实例的检查方法,它可以检查 Person 实例的 String 字段长度是否满足 @Range 的定义:
void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
// 遍历所有 Field:
for (Field field : person.getClass().getFields()) {
// 获取 Field 定义的 @Range:
Range range = field.getAnnotation(Range.class);
// 如果 @Range 存在:
if (range != null) {
// 获取 Field 的值:
Object value = field.get(person);
// 如果值是 String:
if (value instanceof String s) {
// 判断值是否满足 @Range 的 min/max:
if (s.length() < range.min() || s.length() > range.max()) {
throw new IllegalArgumentException("Invalid field: " + field.getName());
}
}
}
}
}
这样一来,我们通过 @Range 注解,配合 check()方法,就可以完成 Person 实例的检查。注意检查逻辑完全是我们自己编写的,JVM 不会自动给注解添加任何额外的逻辑。
泛型
泛型是一种“代码模板”,可以用一套代码套用各种类型。由编译器针对类型作检查。既实现多种类型共用一套代码,又通过编译器保证了类型安全,这就是泛型。
ArrayList<String> strList = new ArrayList<String>();
strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!
向上转型
在 Java 标准库中的 ArrayList<T> 实现了 List<T> 接口,它可以向上转型为 List<T>:
List<String> list = new ArrayList<String>();
要特别注意:不能把 ArrayList<Integer> 向上转型为 ArrayList<Number> 或 List<Number>。
使用泛型
使用 ArrayList 时,如果不定义泛型类型时,泛型类型实际上就是 Object。
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0); // 强制转型
String second = (String) list.get(1);
当 T 为 String 类型:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
String first = list.get(0);
String second = list.get(1);
当 T 为 Number 类型:
List<Number> list = new ArrayList<Number>();
list.add(new Integer(123));
list.add(new Double(12.34));
Number first = list.get(0);
Number second = list.get(1);
编译器如果能自动推断出泛型类型,就可以省略后面的泛型类型。
`List<Number> list = new ArrayList<Number>();
// 可以省略后面的 Number,编译器可以自动推断泛型类型:
List<Number> list = new ArrayList<>();
泛型接口
除了 ArrayList<T> 使用了泛型,还可以在接口中使用泛型。例如 Arrays.sort(Object[]) 可以对任意数组进行排序,但待排序的元素必须实现 Comparable<T> 这个泛型接口。
public interface Comparable<T> {
int compareTo(T o);
}
可以直接对 String 数组进行排序,这是因为 String 本身已经实现了 Comparable<String> 接口。
自定义类型可以通过实现 Comparable<T> 泛型接口来实现排序。
class Person implements Comparable<Person> {
String name;
int score;
Person(String name, int score) {
this.name = name;
this.score = score;
}
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
public String toString() {
return this.name + "," + this.score;
}
}
编写泛型
编写泛型类比普通类要复杂。泛型类一般用在集合类中,例如 ArrayList<T> ,我们很少需要编写泛型类。
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
编写泛型类时要特别注意,泛型类型 <T> 不能用于静态方法。
多个泛型类型
泛型还可以定义多种类型。例如我们希望 Pair 不总是存储两个类型一样的对象,就可以使用类型<T, K>:
public class Pair<T, K> {
private T first;
private K last;
public Pair(T first, K last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public K getLast() { ... }
}
使用的时候,需要指出两种类型:Pair<String, Integer> p = new Pair<>("test", 123);
Java 标准库的 Map<K, V>就是使用两种泛型类型的例子。它对 Key 使用一种类型,对 Value 使用另一种类型。
类型擦除
泛型是一种类似”模板代码“的技术,不同语言的泛型实现方式不一定相同。Java 语言的泛型实现方式是类型擦除。类型擦除是指虚拟机对泛型其实一无所知,所有的工作都是编译器做的。Java 的泛型是由编译器在编译时实行的,编译器内部永远把所有类型 T 视为 Object 处理,但是在需要转型的时候,编译器会根据 T 的类型自动为我们实行安全地强制转型。
了解了 Java 泛型的实现方式是类型擦除,我们就知道了 Java 泛型的局限:
<T>不能是基本类型,例如 int,因为实际类型是 Object,Object 类型无法持有基本类型。- 无法取得带泛型的 Class。所有泛型实例的
getClass()方法返回同一个 Class 实例。 - 无法用 instanceof 判断带泛型的类型。
- 不能实例化 T 类型。
要实例化 T 类型,我们必须借助额外的 Class<T> 参数。
public class Pair<T> {
private T first;
private T last;
public Pair(Class<T> clazz) {
first = clazz.newInstance();
last = clazz.newInstance();
}
}
上述代码借助 Class<T> 参数并通过反射来实例化 T 类型,使用的时候也必须传入 Class<T>。例如 Pair<String> pair = new Pair<>(String.class); 。因为传入了 Class<String> 的实例,我们借助 String.class 就可以实例化 String 类型。
不恰当的覆写方法
有些时候一个看似正确定义的方法会无法通过编译。这是因为定义的 equals(T t) 方法实际上会被类型擦除成 equals(Object t),而这个方法是继承自 Object 的,编译器会阻止一个实际上会变成覆写的泛型方法定义。换个方法名,避开与 Object.equals(Object) 的冲突就可以成功编译。
public class Pair<T> {
public boolean equals(T t) {
return this == t;
}
}
泛型继承
一个类可以继承自一个泛型类。
public class IntPair extends Pair<Integer> {
}
使用的时候,因为子类 IntPair 并没有泛型类型,正常使用即可:IntPair ip = new IntPair(1, 2); 。
在父类是泛型类型的情况下,编译器就必须把类型 T(对 IntPair 来说,也就是 Integer 类型)保存到子类的 class 文件中,不然编译器就不知道 IntPair 只能存取 Integer 这种类型。在继承了泛型类型的情况下,子类可以获取父类的泛型类型。例如 IntPair 可以获取到父类的泛型类型 Integer。
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
public class Main {
public static void main(String[] args) {
Class<IntPair> clazz = IntPair.class;
Type t = clazz.getGenericSuperclass();
if (t instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) t;
Type[] types = pt.getActualTypeArguments(); // 可能有多个泛型类型
Type firstType = types[0]; // 取第一个泛型类型
Class<?> typeClass = (Class<?>) firstType;
System.out.println(typeClass); // Integer
}
}
}
extends 上界通配符
使用类似 <T extends Number> 定义泛型类时表示:泛型类型 T 限定为 Number 以及 Number 的子类。这种使用 <? extends Number> 的泛型定义称之为上界通配符,即把泛型类型 T 的上界限定在 Number 了。
public class Main {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
int n = add(p);
System.out.println(n);
}
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
// 以下两行无法编译:如果 p 是 Pair<Double>,setFirst(Double first) 无法接受 Integer 类型
// p.setFirst(new Integer(first.intValue() + 100));
// p.setLast(new Integer(last.intValue() + 100));
// 以下两行无法编译: Number 是抽象类,无法被实例化
// p.setFirst(new Number(first.intValue() + 100));
// p.setLast(new Number(last.intValue() + 100));
return first.intValue() + last.intValue();
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}
若 add 方法签名变成:static int add(Pair<Number> p),给 add 方法传入 Pair<Integer> 类型时将会编译报错,因为 Pair<Integer> 不是 Pair<Number> 的子类。给 add 方法传入 Pair<Integer> 类型时,它符合参数 Pair<? extends Number> 类型。对 Pair<? extends Number> 类型调用 getFirst() 方法,实际的方法签名变成了:
<? extends Number> getFirst(); ,即返回值是Number或Number的子类,因此可以安全赋值给Number类型的变量:Number x = p.getFirst();。无法传递任何 Number 的子类型给 setFirst(? extends Number)。
因此使用类似 <? extends Number> 通配符作为方法参数时表示:方法内部可以调用获取 Number 引用的方法,例如 Number n = obj.getFirst(); ,方法内部无法调用传入 Number 引用的方法(null 除外),例如 obj.setFirst(Number n); 。一句话总结:使用 extends 通配符为方法参数时表示可读不可写,例如方法参数类型 List<? extends Integer> 表明了该方法内部只会读取List的元素,不会修改List的元素。
在定义泛型类型 Pair<T> 的时候,也可以使用 extends 通配符来限定 T 的类型:public class Pair<T extends Number> { ... } 。非 Number 类型将无法通过编译。
super 通配符
Pair<? super Integer>表示,方法参数接受所有泛型类型为 Integer 或 Integer 父类的 Pair 类型。
public class Main {
public static void main(String[] args) {
Pair<Number> p1 = new Pair<>(12.3, 4.56);
Pair<Integer> p2 = new Pair<>(123, 456);
setSame(p1, 100);
setSame(p2, 200);
System.out.println(p1.getFirst() + ", " + p1.getLast());
System.out.println(p2.getFirst() + ", " + p2.getLast());
}
static void setSame(Pair<? super Integer> p, Integer n) {
p.setFirst(n);
p.setLast(n);
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}
无法使用 Integer 类型来接收 getFirst() 的返回值,即下面的语句将无法通过编译:Integer x = p.getFirst(); 。因为如果传入的实际类型是 Pair<Number>,编译器无法将 Number 类型转型为 Integer。唯一可以接收 getFirst() 方法返回值的是 Object 类型:Object obj = p.getFirst(); 。
因此,使用 <? super Integer> 通配符表示:允许调用 set(? super Integer) 方法传入 Integer 的引用,不允许调用 get() 方法获得 Integer 的引用。使用<? super Integer> 通配符作为方法参数,表示方法内部代码对于参数只能写不能读。
对比 extends 和 super 通配符
作为方法参数,<? extends T> 类型和 <? super T> 类型的区别在于:<? extends T> 允许调用读方法 T get() 获取 T 的引用,但不允许调用写方法 set(T) 传入 T 的引用(传入 null 除外), 方法内部代码对于参数允许读不允许写; <? super T> 允许调用写方法 set(T) 传入 T 的引用,但不允许调用读方法 T get()获取 T 的引用(获取 Object 除外),方法内部代码对于参数允许写不允许读。
public class Collections {
// 把 src 的每个元素复制到 dest 中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i);
dest.add(t);
}
}
}
copy 方法的作用是把一个 List 的元素依次添加到另一个 List 中。对于类型 <? extends T> 的变量 src,我们可以安全地获取类型 T 的引用,而对于类型 <? super T> 的变量 dest,我们可以安全地传入 T 的引用。这个方法的定义就完美地展示了 extends 和 super 的意图:方法内部不会读取 dest,因为不能调用 dest.get() 来获取 T 的引用;方法内部也不会修改 src,因为不能调用 src.add(T) 。
这个 copy 方法的另一个好处是可以安全地把一个 List<Integer> 添加到 List<Number> ,但是无法反过来添加。
无限定通配符
Java 的泛型允许使用无限定通配符,即只定义一个 ? 。因为 <?> 通配符既没有 extends,也没有 super,因此不允许调用 set(T) 方法并传入引用(null 除外);
不允许调用 T get()方法并获取 T 引用(只能获取 Object 引用),既不能读也不能写。
void sample(Pair<?> p) {
}
只能做一些null判断:
static boolean isNull(Pair<?> p) {
return p.getFirst() == null || p.getLast() == null;
}
大多数情况下,可以引入泛型参数 <T> 消除 <?> 通配符:
static <T> boolean isNull(Pair<T> p) {
return p.getFirst() == null || p.getLast() == null;
}
<?> 通配符有一个独特的特点:Pair<?> 是所有 Pair<T> 的超类。
泛型和反射
Java 的部分反射 API 也是泛型。例如 Class<T> 就是泛型:
Class clazz = String.class;
String str = (String) clazz.newInstance();
Class<String> clazz = String.class;
String str = clazz.newInstance();
调用 Class 的 getSuperclass() 方法返回的 Class 类型是 Class<? super T>:Class<? super String> sup = String.class.getSuperclass(); 。
构造方法 Constructor<T> 也是泛型:
Class<Integer> clazz = Integer.class;
Constructor<Integer> cons = clazz.getConstructor(int.class);
Integer i = cons.newInstance(123);
我们可以声明带泛型的数组,但不能用 new 操作符创建带泛型的数组,必须通过强制转型实现带泛型的数组
Pair<String>[] ps = null; // ok
// Pair<String>[] ps = new Pair<String>[2]; // compile error!
@SuppressWarnings("unchecked")
Pair<String>[] ps = (Pair<String>[]) new Pair[2];
借助 Class<T>来创建泛型数组:
T[] createArray(Class<T> cls) {
return (T[]) Array.newInstance(cls, 5);
}
浙公网安备 33010602011771号