注解、反射、枚举


一、注解

1、何为注解

  • Annotation 是从JDK5.0开始引入的新技术 .
  • Annotation的作用
    • 不是程序本身 , 可以对程序作出解释.(这一点和注释(comment)没什么区别)
    • 可以被其他程序(比如:编译器等)读取.
  • Annotation的格式
    • 注解是以”@注释名”在代码中存在的
    • 还可以添加一些参数值 , 例如:@SuppressWarnings(value=”unchecked”)
  • Annotation在哪里使用?
    • 可以附加在package , class , method , field 等上面 , 相当于给他们添加了额外的辅助信息
    • 我们可以通过反射机制实现对这些元数据的访问

2、JDK内置注解

(1)@Override

限定重写父类方法, 实现接口方法。该注解只能用于方法

(2)@Deprecated

用于表示所修饰的元素(类, 方法,构造器等等)已过时。通常是因为所修饰的结构危险或存在更好的选择,过时是可以用的,意义只是作为一种提示,因为原来的项目中用的老的代码必须要能用,过时是给我们后面做开发的提示

(3)@SuppressWarnings

抑制编译器警告

警告信息在eclipse中更明显一些

比如在eclipse的main中:int i=10;但程序中没有用到i,在这一行左边就会出现黄颜色的感叹号警告,但如果在这一行或者main方法上面的那一行加了@SuppressWarnings(“unused”),警告就没有了。这个注解的值叫做unused,在IDEA中没有黄颜色的感叹号警告,如果变量没有使用,变量的颜色是灰色的。在定义变量的那一行上方加上@SuppressWarnings(“unused”),变量也就变成黑色了

比如在eclipse中,如果声明ArrayList没有用泛型,list没有用都会有警告,可以在上面加上@SuppressWarnings({“unused”,“rawtypes”}),警告就没有了

@SuppressWarnings("all") 
@SuppressWarnings("unchecked") 
@SuppressWarnings(value={"unchecked","deprecation"})

3、元注解

  • 元注解的作用就是负责注解其他注解 , Java定义了4个标准的meta-annotation类型,他们被用来提供 对其他annotation类型作说明 .
  • 这些类型和它们所支持的类在java.lang.annotation包中可以找到 .( @Target , @Retention , @Documented , @Inherited )
    • @Target : 用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
    • @Retention : 表示需要在什么级别保存该注释信息 , 用于描述注解的生命周期 (SOURCE < CLASS < RUNTIME) (一般自定义注解都设置为 RUNTIME)
    • @Document:说明该注解将被包含在javadoc中
    • @Inherited:说明子类可以继承父类中的该注解

(1)@Target

参数值 可用范围
TYPE 类、接口、枚举类型
FIELD 字段属性
METHOD 方法
PARAMETER 方法参数
CONSTRUCTOR 构造函数
LOCAL_VARIABLE 局部变量
ANNOTATION_TYPE 注解
PACKAGE

4、自定义注解

  • 使用 @interface自定义注解时 , 自动继承了java.lang.annotation.Annotation接口
  • 分析 : @ interface用来声明一个注解 , 格式 : public @ interface 注解名 { 定义内容 }
    • 其中的每一个方法实际上是声明了一个配置参数.
    • 方法的名称就是参数的名称.
    • 返回值类型就是参数的类型 ( 返回值只能是基本类型,Class , String , enum ).
    • 可以通过default来声明参数的默认值
    • 如果只有一个参数成员 , 一般参数名为value
    • 注解元素必须要有值 , 我们定义注解元素时 , 经常使用空字符串,0作为默认值 .
// 用于描述注解的使用范围,数组类型,需要多个值,用{}
@Target(value = {ElementType.TYPE,ElementType.FIELD})
// 表示需要在什么级别保存该注释信息
@Retention(value = RetentionPolicy.RUNTIME)
// 说明该注解将被包含在javadoc中
@Documented
// 说明子类可以继承父类中的该注解
@Inherited
public @interface MyAnnotation {

    // 枚举类型,注解的参数,MyValue自定义枚举类型
    MyValue value();

    // 注解参数:数组类型,默认为空
    String[] scanBasePackages() default {};

    // 注解参数:类型为布尔,默认值true
    boolean proxyBeanMethods() default true;
}

5、反射读取类注解参数

try{
    // 1、反射,Class可以获得类的全部信息
    Class<?> studentClass = Class.forName("com.rewind.code01.annotation.Student");

    // 2.1、获取该类的所有注解
    Annotation[] annotations = studentClass.getAnnotations();
    for (Annotation annotation:annotations){
        System.out.println(annotation);
    }

    // 2.2、获得类指定注解的value值
    MyAnnotation myAnno = (MyAnnotation)studentClass.getAnnotation(MyAnnotation.class);
    System.out.println(myAnno.value());

    // 2.3、获取类指定字段
    Field name = studentClass.getDeclaredField("name");
    System.out.println(name);
    // 获取该字段上指定注解的值
    MyAnnotation annotation = name.getAnnotation(MyAnnotation.class);
    System.out.println(annotation.value());


}catch (Exception e){
    e.printStackTrace();
}

二、反射

1、静态语言 VS 动态语言

动态语言

可以在运行时改变结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构

主要动态语言:c#、JavaScript、PHP、Python

静态语言

与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++

Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动态性,我们可以利用反射机制获得类似动态语言的特性。

2、Java Reflection

Reflection(反射)是 Java 被视为动态语言的关键,反射机制允许程序在执行期借助于 Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。

Class c = class.forName("java.lang.String");

加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这 个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子, 透过这个镜子看到类的结构,所以,我们形象的称之为:反射

image-20210727154909842

以下反射代码均以该对象为示例

@Data
@NoArgsConstructor
@AllArgsConstructor
@MyAnnotation(value = MyValue.User)
public class User {
    @MyAnnotation(value = MyValue.NAME)
    private String name;

    private int age;

    private String method(String s){
        return s;
    }
}

3、获取Class文件方式

(1)getClass() 通过对象获取

调用person类的父类object方法getClaass();

// 通过对象获取 class 文件
User user = new User("张三",12);
Class<? extends User> userClass = user.getClass();

(2).class 通过类型获取

每个类型(包括基本类型和引用)都有一个静态属性,class。

// 通过类名获取 class 文件
Class<User> userClass2 = User.class;

(3).forName() 通过全包名获取

Class类的静态方法获取。forName(“字符串的类名”)写全名,要带包名。 (包名.类名)

包名类名不存在抛出异常

// 通过全包名获取 class 文件
try {
    Class<?> userClass3 = Class.forName("com.rewind.code01.entity.User");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

(4).TYPE 包装类获取class文件

// 基本数据类型的包装类获取 class 文件
Class<Byte> byteClass = Byte.TYPE;
Class<Boolean> booleanClass = Boolean.TYPE;
Class<Integer> integerClass = Integer.TYPE;

(5)获取父类的 class 文件

// 获取父类的 class 文件
Class<?> superclass = userClass1.getSuperclass();

4、获取类的信息

(1)获取所有构造器

通过getDeclaredConstructors可以返回类的所有构造方法,返回的是一个数组因为构造方法可能不止一个,通过getModifiers可以得到构造方法的类型,getParameterTypes可以得到构造方法的所有参数,返回的是一个Class数组

Class<User> userClass = User.class;

// 获取所有构造函数
Constructor<?>[] constructors = userClass.getDeclaredConstructors();

// 获取构造函数信息
for (int i = 0; i < constructors.length; i++) {
    // getModifiers():获取构造方法的类型
    System.out.print(Modifier.toString(constructors[i].getModifiers()) + "参数:");
    // .getParameterTypes(): 获取构造方法的参数
    Class[] parametertypes = constructors[i].getParameterTypes();
    for (int j = 0; j < parametertypes.length; j++) {
        System.out.print(parametertypes[j].getName() + " ");
    }
    System.out.println("");
}
public参数:java.lang.String int 
public参数:

获取 public 类型的构造器

Constructor<?>[] constructors1 = userClass.getConstructors();

(2)获取指定的构造器

我们可以通过getDeclaredConstructor()方法传参获取特定参数类型的构造方法,这里注意是getDeclaredConstructor()不是 getDeclaredConstructors() ,所以返回的是一个Class对象而不是一个Class数组。

// 获取指定的构造方法
try {
    // 获取空参的构造器
    Constructor<User> constructor = userClass.getDeclaredConstructor();
    System.out.println(constructor.toString());

    // 获取参数为 String 和 int 的构造方法
    Class[] classes = {String.class,int.class};
    Constructor<User> constructor1 = userClass.getConstructor(classes);
    System.out.print(Modifier.toString(constructor1.getModifiers()) + "参数:");
    Class[] parametertypes = constructor1.getParameterTypes();
    for (int j = 0; j < parametertypes.length; j++) {
        System.out.print(parametertypes[j].getName() + " ");
    }

} catch (NoSuchMethodException e) {
    e.printStackTrace();
}

5、通过构造器创建对象

Class<User> userClass = User.class;

try {
    Class[] paramTypes = {String.class,int.class};
    Constructor<User> constructor = userClass.getDeclaredConstructor(paramTypes);
    // 该行代码用于获取 私有构造器
    // constructor.setAccessible(true);
    // newInstance() 通过class的构造器创建对象
    User user = constructor.newInstance("张三",20);
} catch (Exception e) {
    e.printStackTrace();
}

6、调用类的方法

我们知道如果我们要正常的调用类的方法都是通过类.方法调用,所以我们调用私有方法也需要得到类的实例,而我们上面newInstace已经得到了类的实例,这样就好办了。

我们首先通过 getDeclaredMethod方法获取到这个私有方法,第一个参数是方法名,第二个参数是参数类型

然后通过invoke方法执行,invoke需要两个参数一个是类的实例,一个是方法参数。

类的实例当不能new 获取的时候我们也可以通过反射获取,就是下面的newInstance方法。

Class<User> userClass = User.class;

// ---------------------获取实例对象----------------------
Class[] paramTypes1 = {String.class,int.class};
Constructor<User> constructor = userClass.getConstructor(paramTypes1);
User user = constructor.newInstance("张三", 12);

// ---------------------执行对象的私有方法---------------------
Class[] paramTypes2 = {String.class};
// 获取类的方法,参数:方法名,方法参数类型数组
Method method = userClass.getDeclaredMethod("method", paramTypes2);
// 使私有方法允许访问
method.setAccessible(true);
// 方法执行,参数:类的实例对象,方法参数值
String s = (String) method.invoke(user, "方法执行了。。。");
System.out.println(s);  //方法执行了。。。

7、获取类的字段并修改

看到这里你可能会说,有了set方法,什么私有不私有,set不就可以了,但是这里要注意我们是没有办法得到这个类的实例的,要不然都可以得到实例就没有反射一说了。我们在通过反射得到类的实例之后先获取字段

Class<User> userClass = User.class;

// ----------------------获取实例对象-------------------------
Class[] paramTypes1 = {String.class,int.class};
Constructor<User> constructor = userClass.getConstructor(paramTypes1);
User user = constructor.newInstance("张三", 12);

// --------------------获取私有属性并修改------------------------
// 获取 class 中的字段
Field age = userClass.getDeclaredField("age");
// 允许私有访问
age.setAccessible(true);
// 修改值,参数:实例对象,属性值
age.set(user,15);
// 获取对象属性值,参数:实例对象
int o = (int)age.get(user);
System.out.println(o);  // 15
  1. getDeclaredFiled 仅能获取类本身的属性成员(包括私有、共有、保护)
  2. getField 仅能获取类(及其父类可以自己测试) public属性成员

因此在获取父类的私有属性时,要通过getSuperclass的之后再通过getDeclaredFiled获取。

// getField 只能获取类及其父类的 public 属性
Field publicField = re.getClass().getField("publicField");

// getDeclaredFiled 只能获取到当前类的属性, 但是能所有属性(包括私有、共有、保护)
Field privateField = re.getClass().getDeclaredField("privateField");

//获取父类私有属性并获取值
Field fileId = re.getClass().getSuperclass().getDeclaredField("id");
fileId.setAccessible(true);
System.out.println(fileId.get(re));

8、反射操作注解

同上一、5

9、反射操作泛型

  • Java采用泛型擦除的机制来引入泛型 , Java中的泛型仅仅是给编译器javac使用的,确保数据的安全性和免去强制类型转换问题 , 但是 , 一旦编译完成 , 所有和泛型有关的类型全部擦除
  • 为了通过反射操作这些类型 , Java新增了 ParameterizedType , GenericArrayType , TypeVariable 和 WildcardType 几种类型来代表不能被归一到Class类中的类型但是又和原始类型齐名的类型.
  • ParameterizedType : 表示一种参数化类型,比如Collection
  • GenericArrayType : 表示一种元素类型是参数化类型或者类型变量的数组类型
  • TypeVariable : 是各种类型变量的公共父接口
  • WildcardType : 代表一种通配符类型表达式

10、setAccessible

  • Method和Field、Constructor对象都有setAccessible()方法。

  • setAccessible作用是启动和禁用访问安全检查的开关。

  • 参数值为true则指示反射的对象在使用时应该取消Java语言访问检查。

  • 提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true。

  • 使得原本无法访问的私有成员也可以访问

  • 参数值为false则指示反射的对象应该实施Java语言访问检查

11、反射ORM案例

(1)注解

/**
 * 该注解用于类上,用户获取表名
 */
@Target(value = ElementType.TYPE)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface MyTableName {
    String value();
}
/**
 * 该注解用于字段上,用于获取字段名和字段的值
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyFieldName {
    String columnName();
    String type();
}

(2)实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
@MyTableName(value = "t_user")
public class User {

    @MyFieldName(columnName = "id",type = "bigint")
    private Long id;

    @MyFieldName(columnName = "name",type = "varchar")
    private String name;

    @MyFieldName(columnName = "age",type = "int")
    private int age;
}

(3)实现

public class Test01<T> {

    public static void main(String[] args) throws Exception {
        // 根据该对象生成 sql 语句
        User user = new User(123L, "张三", 15);
        Test01<User> test01 = new Test01<>();
        String sql = test01.insert(user);
        System.out.println(sql);
    }

    /**
     * 案例:传入一个 User 对象,根据该对象生成 mysql 新增语句
     * @param args
     */
    public String insert(T t) throws IllegalAccessException {
        Class<T> tClass = (Class<T>) t.getClass();
        // 1、获取实体类上的 MyTableName 的值,即表名
        String tableName = tClass.getAnnotation(MyTableName.class).value();

        // 拼接 sql
        StringBuilder sql = new StringBuilder();
        sql.append("insert into ");
        sql.append(tableName);

        HashMap<String, Object> map = new HashMap<>();

        // 2、获取实体类上所有的字段
        Field[] fields = tClass.getDeclaredFields();
        for (Field field : fields) {
            // 3.1 获取字段上的 MyFieldName 注解
            MyFieldName annotation = field.getAnnotation(MyFieldName.class);
            // 3.2 获取 字段名 和 字段类型
            String columnName = annotation.columnName();
            String type = annotation.type();
            // 3.3 获取 t 对象中该字段的值
            field.setAccessible(true);
            Object o = field.get(t);
            // 3.4 获取字段名和字段类型
            String name = field.getName();
            Class<?> fieldType = field.getType();

            map.put(columnName,o.toString());
        }

        sql.append(" (");
        for(String key :map.keySet()){
            sql.append(key);
            sql.append(",");
        }
        int length1 = sql.length();
        sql.delete(length1-1,length1);
        sql.append(") values(");
        for(String key :map.keySet()){
            sql.append("'");
            sql.append(map.get(key));
            sql.append("'");
            sql.append(",");
        }
        int length2 = sql.length();
        sql.delete(length2-1,length2);
        sql.append(")");
        return sql.toString();
    }
}

三、枚举

  • 枚举的理解: 类的对象只有确定的有限个,即可称该类为枚举类
  • 定义一组常量,通常使用枚举类
  • 枚举类只有一个对象时,则可作为单例模式的实现方式

1、JDK1.5之前实现

public class Season {
    // 1、声明枚举对象属性:private final修饰
    private final String seasonName;
    private final String seasonDesc;

    // 2、私有化构造器,防止外部调用,并可对属性进行赋值
    private Season(String seasonName, String seasonDesc){
        this.seasonName=seasonName;
        this.seasonDesc=seasonDesc;
    }

    // 3、创建对象,提供当前枚举类的多个对象
    public static final Season SPRING = new Season("春天","很爽的季节");
    public static final Season SUMMER = new Season("夏天","热死人的季节");
    public static final Season AUTUMN = new Season("秋天","挺爽的季节");
    public static final Season WINTER = new Season("冬天","被窝里的季节");

    // 4、提供获取枚举对象属性的方法
    public String getSeasonName() {
        return seasonName;
    }

    public String getSeasonDesc() {
        return seasonDesc;
    }

    // 5、toString
    @Override
    public String toString() {
        return "Season{" +
                "seasonName='" + seasonName + '\'' +
                ", seasonDesc='" + seasonDesc + '\'' +
                '}';
    }
}
public static void main(String[] args) {
    Season spring = Season.SPRING;
    System.out.println(spring);
}
Season{seasonName='春天', seasonDesc='很爽的季节'}

2、JDK1.5之后实现

JDK1.5 引入了 Enum 关键字,可用于定义枚举类。

  • Enum定义的枚举类不需要重写 toString 方法,默认输出对象名。
  • 所有的枚举类继承于 java.lang.Enum
  • Enum类可以实现接口并且实现方法
public enum Season2 {
    // 1、提供当前枚举类的对象,多个对象之间用逗号连接,最后一个对象用分号结尾
    // 此处相当于 JDK1.5 之前的创建对象
    // 对象名(构造函数的参数1,参数2)
    SPRING("春天","有点热"),
    SUMMER("夏天","非常热"),
    AUTUMN("秋天","挺热的"),
    WINTER("冬天","有点冷");

    // 2、声明枚举对象属性:private final修饰
    private final String seasonName;
    private final String seasonDesc;

    // 3、私有化构造器,防止外部调用,并可对属性进行赋值
    private Season2(String seasonName, String seasonDesc){
        this.seasonName=seasonName;
        this.seasonDesc=seasonDesc;
    }

    public String getSeasonName() {
        return seasonName;
    }

    public String getSeasonDesc() {
        return seasonDesc;
    }

    @Override
    public String toString() {
        return "Season2{" +
                "seasonName='" + seasonName + '\'' +
                ", seasonDesc='" + seasonDesc + '\'' +
                '}';
    }
}
System.out.println(Season2.SPRING);
// 默认输出
SPRING
// 重写toString
Season2{seasonName='春天', seasonDesc='有点热'}    

3、Enum 的主要方法

  • values() : 返回枚举类型的对象数组,方便遍历所有的枚举值
  • valuesOd(String str) : 根据枚举对象名获取枚举对象,如果不存与参数字符串相同的对象名,则报错
// 返回所有的枚举对象数组
Season2[] values = Season2.values();
for (Season2 value : values) {
    System.out.println(value);
}

// 根据枚举对象名获取枚举对象
Season2 spring = Season2.valueOf("SPRING");
System.out.println(spring);

4、Enum实现接口

接口实现方式有2种

  • 类级别的实现:所有没有实现该方法的枚举对象,执行接口方法时,都执行该方法
  • 匿名内部类的形式实现:所有枚举对象分别实现方法。即不同的枚举对象执行同一方法时,执行的代码不同
public enum Season2 implements EnumInterface {

    SPRING("春天","有点热"){
        @Override
        public void run() {
            System.out.println("春天跑了");
        }
    },

    SUMMER("夏天","非常热"){
        @Override
        public void run() {
            System.out.println("夏天跑了");
        }
        @Override
        public void show(){
            System.out.println("夏天自己的show()方法,覆盖公共show()方法");
        }
    },

    AUTUMN("秋天","挺热的"){
        @Override
        public void run() {
            System.out.println("秋天跑了");
        }
    },

    WINTER("冬天","有点冷"){
        @Override
        public void run() {
            System.out.println("冬天跑了");
        }
    };

    // 2、声明枚举对象属性:private final修饰
    private final String seasonName;
    private final String seasonDesc;

    // 3、私有化构造器,防止外部调用,并可对属性进行赋值
    private Season2(String seasonName, String seasonDesc){
        this.seasonName=seasonName;
        this.seasonDesc=seasonDesc;
    }

    public String getSeasonName() {
        return seasonName;
    }

    public String getSeasonDesc() {
        return seasonDesc;
    }

    @Override
    public String toString() {
        return "Season2{" +
                "seasonName='" + seasonName + '\'' +
                ", seasonDesc='" + seasonDesc + '\'' +
                '}';
    }

    @Override
    public void show() {
        System.out.println("公共show()方法");
    }
}
public static void main(String[] args) {
    Season2.SPRING.show();
    Season2.SPRING.run();
    Season2.SUMMER.show();
    Season2.SUMMER.run();
}
公共show()方法
春天跑了
夏天自己的show()方法,覆盖公共show()方法
夏天跑了

四、泛型

1、诞生背景

List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);

for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
    Log.d("泛型测试","item = " + item);
}

运行上面代码时,毫无疑问是会报错的

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

本例向list类型集合中加入了一个字符串类型的值和一个Integer类型的值(这样是合法的,因为此时list默认的类型为Object类型)。

使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。

2、泛型擦除

泛型只在编译阶段有效

ArrayList<String> a = new ArrayList<String>();
ArrayList b = new ArrayList();
Class c1 = a.getClass();
Class c2 = b.getClass();
System.out.println(c1 == c2); //true

上面程序的输出结果为true。因为所有反射的操作都是在运行时的,既然为true,就证明了编译之后,程序已经去除了泛型化。

也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,成功编译过后的class文件中是不包含任何泛型信息的。

上述结论可通过下面反射的例子来印证:

ArrayList<String> a = new ArrayList<String>();
a.add("rewind");
Class c = a.getClass();
try{
     Method method = c.getMethod("add",Object.class);
     method.invoke(a,100);

}catch(Exception e){
    e.printStackTrace();
}
System.out.println(a);    // ["rewind", 100]

因为绕过了编译阶段也就绕过了泛型

3、泛型的使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型

(1)泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

泛型类的最基本写法:

class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
    private 泛型标识 /*(成员变量类型)*/ var; 
    .....

    }
}

一个最普通的泛型类:

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{ 
    //key这个成员变量的类型为T,T的类型由外部指定  
    private T key;

    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
//传入的实参类型需与泛型的类型参数类型相同,即为Integer.
Generic<Integer> genericInteger = new Generic<Integer>(123456);

//传入的实参类型需与泛型的类型参数类型相同,即为String.
Generic<String> genericString = new Generic<String>("key_vlaue");
Log.d("泛型测试","key is " + genericInteger.getKey());
Log.d("泛型测试","key is " + genericString.getKey());

//不传递泛型类型实参,则泛型不起作用
Generic generic1 = new Generic(4444);

定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

注意

  • 泛型的类型参数只能是类类型,不能是基本数据类型。

  • 不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。

    if(ex_num instanceof Generic<Number>){   
    } 

(2)泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中。

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时:

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

(3)泛型方法

泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型

/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *     3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 */
public <T> T genericMethod(Class<T> tClass)throws Exception{
        T instance = tClass.newInstance();
        return instance;
}

//------------------------------------------------------------------

class GenerateTest<T>{
    public void show_1(T t){
        System.out.println(t.toString());
    }

    //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
    //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
    public <E> void show_3(E t){
        System.out.println(t.toString());
    }

    //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
    public <T> void show_2(T t){
        System.out.println(t.toString());
    }
}

泛型方法与可变参数

public <T> void printMsg( T... args){
    for(T t : args){
        System.out.println("泛型测试","t is " + t);
    }
}

其他情况

public class GenericTest {
   //这个类是个泛型类,在上面已经介绍过
   public class Generic<T>{     
        private T key;

        public Generic(T key) {
            this.key = key;
        }

        //虽然在方法中使用了泛型,但是这并不是一个泛型方法。
        //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
        //所以在这个方法中才可以继续使用 T 这个泛型。
        public T getKey(){
            return key;
        }

        /**
         * 这个方法是有问题的,在编译器会抛出错误信息"cannot reslove symbol E"
         * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
        public E setKey(E key){
             this.key = keu
        }
        */
    }

    /** 
     * 这才是一个真正的泛型方法。
     * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
     * 这个T可以出现在这个泛型方法的任意位置.
     * 泛型的数量也可以为任意多个 
     *    如:public <T,K> K showKeyName(Generic<T> container){
     *        ...
     *        }
     */
    public <T> T showKeyName(Generic<T> container){
        System.out.println("container key :" + container.getKey());
        //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
        T test = container.getKey();
        return test;
    }

    //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
    public void showKeyValue1(Generic<Number> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }

    //这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
    //同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
    public void showKeyValue2(Generic<?> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }

     /**
     * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
     * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
     * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
    public <T> T showKeyName(Generic<E> container){
        ...
    }  
    */

    /**
     * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
     * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
     * 所以这也不是一个正确的泛型方法声明。
    public void showkey(T genericObj){

    }
    */
}

(4)静态方法与泛型

静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法

public class StaticGenerator<T> {
    ....
    ....
    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void show(T t){

    }
}

4、泛型通配符

public void showKeyValue1(Generic<Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gNumber);

// showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer> 
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);

通过提示信息我们可以看到Generic<Integer>不能被看作为``Generic`的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的

回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理 Generic<Integer> 类型的类,这显然与java中的多台理念相违背。因此我们需要一个在逻辑上可以表示同时是Generic<Integer>Generic<Number> 父类的引用类型。由此类型通配符应运而生。

我们可以将上面的方法改一下:

public void showKeyValue1(Generic<?> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

类型通配符一般是使用代替具体的类型实参,注意了,此处是类型实参,而不是类型形参 。此处的?和Number、String、Integer一样都是一种实际的类型,可以把看成所有类型的父类。是一种真实的类型。

可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。

5、上下限(协变/逆变)

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

泛型的上限:

  • 格式 类型名称 <? extends 类 > 对象名称
  • 意义只能接收该类及其子类

泛型的下限:

  • 格式: 类型名称 <? super 类 > 对象名称
  • 意义: 只能接收该类及其父类型

协变以及逆变概念

  • 协变指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型。extends/向上转换/不能add/只能get(T及父类)
  • 逆变指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型 。 super/向下转换/不能get/只能add(T及子类)

泛型类型参数支持协变和逆变,可在分配和使用泛型类型方面提供更大的灵活性。

image-20220913221039126

由于 JAVA 有多态的特性,可以用父类接收子类:

  • 协变,添加时,因无法确定通配符子类的下限,故无法添加数据

  • 逆变,添加时,可添加下限的子类,从而被父类接收。

区别

<? extends Father >:通配符的内容需要继承自 Father

  • 获取数据时:可以确定类型就是 Father 或者 Father 的父类(类只有一个唯一父类)
  • 添加数据时:因为 Father 的子类可能有多个,无法确定是要添加哪种类型,所以无法添加

<? super Father >:通配符的内容需要是 Father 的父类

  • 获取数据时:无法确定具体是 Father 的哪一个父类,故只能赋值给最顶层父类 Object
  • 添加数据时:只要是 Father 及其子类均可添加,因为可以确定他们都是继承自 Father

使用场景

  • 使用上边界(extends)时,无法添加元素,但是可以轻易的获取元素。(适合 读取
  • 使用下边界(super)时,可以很容易的添加元素,但是获取元素的时候只能用Object类型的引用接收。(适合写入

(1)协变

  • 协变指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型。

利用通配符实现泛型的协变:<? extends T>**子类通配符;这个通配符定义了?继承自T,可以帮助我们实现向上转换**:

下面所用的类的继承关系:SonTest -> FatherTest -> GrandpaFather

//协变,向上转型
ArrayList<? extends FatherTest> list = new ArrayList<SonTest>();

list.add(new FatherTest());//报错
list.add(new SonTest());//报错
list.add(null);

FatherTest fatherTest = list.get(0);

<? extends FatherTest>看成一个整体,我们能确定 list 的具体类型肯定是 FatherTest 或者FatherTest 的父类(因为一个类只能有一个直接父类,所以确定了FatherTest,那么FatherTest的父类则都是可以确定的)

FatherTest fatherTest = list.get(0);//可成功获取
GrandpaFather grandpaFather = list.get(0);//可成功获取

而不能确定list的类型是FatherTest的子类当中具体的哪一个?(有多个类都继承自FatherTest),所以这也就直接导致了一旦使用了<? extends T>向上转换之后,不能再向list中添加任何类型的对象了,这个时候只能选择从list当中get数据而不能add

list.add(new FatherTest());//报错
list.add(new SonTest());//报错
list.add(null);//可成功添加

在举一个例子

public static void test8(List<? extends FatherTest> list){

    /* --------- 可添加数据范围 ----------- */
    // 定义了泛型上限,则是协变,无法存储数据,以下add()均报错
    list.add(new Object());
    list.add(new GrandpaFather());    
    list.add(new FatherTest());
    list.add(new SonTest());

       /* --------- 获取时可赋值范围 ----------- */
    // 可以获取数据,类型是FatherTest或者其父类
    FatherTest fatherTest = list.get(0);
    GrandpaFather grandpaFather = list.get(0);
    Object object = list.get(0);
}

/* --------- 通配符范围 ----------- */
test8(new ArrayList<GrandpaFather>());    //报错
test8(new ArrayList<FatherTest>());        //正常
test8(new ArrayList<SonTest>());        //正常

在举一个例子

@Test
public void test5(){
    GrandpaFather grandpaFather = test6(new GrandpaFather());   // 报错
    FatherTest fatherTest = test6(new FatherTest());    // 正常
    SonTest sonTest = test6(new SonTest());     // 正常
}

// 
public static <T extends FatherTest> T test6(T t){
    return t;
}

(2)逆变

  • 逆变指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型 。
ArrayList<? super SonTest> list1 = new ArrayList<FatherTest>();

list1.add(new SonTest());   //可成功添加
list1.add(new FatherTest());    //报错
list1.add(null);   //可成功添加

Object o = list1.get(0);

逆变使用通配符 <? super T>(超类通配符),如上面代码,FatherTestSonTest的超类,则这个时候对于JVM来说,它能确定list的类型的超类肯定是SonTest或者SonTest的父类,换言之该类型就是SonTest或者SonTest的子类,所以和上面的协变一样,既然确定了类型的范围,那么list能够add的类型也就是SonTest或者SonTest的子类了。

例子2

public static void test9(List<? super FatherTest> list){

    /* --------- 可添加数据范围 ----------- */
    list.add(new Object());            //报错
    list.add(new GrandpaFather());    //报错
    list.add(new FatherTest());        //正常
    list.add(new SonTest());        //正常

    /* --------- 获取时可赋值范围 ----------- */
    // 泛型下限,获取的对象只能赋值给 Object
    Object object = list.get(0);
}

/* --------- 通配符范围 ----------- */
test9(new ArrayList<Object>());            //正常
test9(new ArrayList<GrandpaFather>());    //正常
test9(new ArrayList<FatherTest>());        //正常
test9(new ArrayList<SonTest>());        //报错

(3)PECS

PECS(producer-extends, consumer-super)

如果参数化类型表示一个生产者,就使用 <? extends T>。比如list.get(0)这种,list作为数据源producer;

如果它表示一个消费者,就使用 <? super T>。比如:list.add(new Apple()),list作为数据处理端consumer。


  目录