Java泛型设计教程


在业务开发中往往会碰到业务逻辑相同的功能,对于不同的项目而言迁移核心代码即可,但对于同个项目而言,最简单的方法就是将这部分代码抽象出来,从而实现代码复用。而除了抽离为抽象类或接口之外, Java 同时提供泛型这一特性供于业务抽象。

如针对数据库中实体对象通过会定义对其的增删改查操作,但在宏观上而言即使不同实体的增删改也仅仅只是针对的对象不同而已,实现逻辑上并没有什么区别。泛型的诞生就是为了简化代码提供减少无意义的代码开发,让开发人员专注于业务, Java 对此提供泛型方法与泛型接口。

一、定义概念

1. 泛型对象

JDK 1.5Java 引入了泛型特性,从而提供了可拓展的代码模块编写。

需要注意在泛型设计中并无法通过 new T() 实现对象初始化,想要在泛型设计中创建泛型指代的类对象,最常用的方式即利用反射实现。

2. 泛型擦除

代码编写阶段程序中所定义的泛型类在编译之后将转为实际对象的类,而非 T 等泛型指代对象,即编译完成的 class 文件中将替换为相应的类,这个过程称为泛型擦除。

@Test
public void demo() {
    return convert(new User());
}

public <T> T convert(T t) {
    return (T) t;
}

如上述的代码示例在编译后的 class 中相对应的代码格式如下:

@Test
public void demo() {
    return convert(new User());
}

public User convert(User user) {
    return (User) user;
}

二、泛型方法

1. 基础介绍

先举个简单的例子来了解一下什么是泛型,在下述示例中定义一个不包含任何业务的方法,将传入的对象原封不动的返回,代码如下:

public User doNothing(User user) {
    // do something to "user"

    return user;
}

是不是很简单,但如果 StudentTeacher 等对象也需要实现类似的操作呢?

在不使用泛型的前提下,就只能为不同的对象定义其相对应的方法,但为逻辑相同仅作用对象不同的操作而重复定义显然过于冗余,下面就再来看看使用泛型的话又应该如何定义?

/**
  * <T> 表示为泛型方法,T 代指传入参数类型
  */
public <T> T doNothing(T t) {
    // do something to "t"

    return t;
}

根据上述的示例可以看出,在方法定义头中通过 <T> 表示为泛型方法,其中的 T 为指代对象。对于一个泛型方法而言,不管传入的参数是什么类型,都能够屏蔽对象类型的差异执行方法体内容,从而实现抽象简化的功能。

2. 类型获取

在编写泛型方法时,当需要获取泛型指代的类对象时并不能直接简单的通过 T.class 实现,这是由于泛型的运行时编译导致的。

因此,在泛型方法中若需要对应的 class 信息,通常有下述两种方式:通过 getClass() 获取后类型强转实现,或者通过形参由调用时传入。

两种不同的定义方式示例如下:

public static <T> void manner1(T t) {
    Class<T> cls = (Class<T>) t.getClass();

    // do something to "cls"
}

public static <T> void manner2(T t, Class<T> cls) {
    // do something to "cls"

}

3. 应用场景

在上面通过一个简单的示例了解了泛型的定义与效果,下面结合简单的业务逻辑加深一下印象。

假如需要实现一个 byte 转换为 User 的功能,如下示例代码中将传入的字节数组转化为需要的用户类:

public User convert(byte[] bytes) {
    Object object;
    try (
            ByteArrayInputStream in = new ByteArrayInputStream(bytes);
            ObjectInputStream ois = new ObjectInputStream(in)
    ) {
        // 将字节转为普通对象
        object = ois.readObject();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    // 将 object 强转为 User 
    return (User) object;
}

随着项目的推进,在后续开发中发现在其它业务中同样有这个需求,不同的是需要将字节数组转化为 Student 类。当然你可以直接修改上面的方法使其返回值 Object ,将类型转化由调用方处理。

但在学习了泛型之后即可很轻易的改造上述代码从而达到通用的效果,改造后的代码如下:

public <T> T convert(byte[] bytes, Class<T> tClass) {
    Object object;
    try (
            ByteArrayInputStream in = new ByteArrayInputStream(bytes);
            ObjectInputStream ois = new ObjectInputStream(in)
    ) {
        // 将字节转为普通对象
        object = ois.readObject();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    // 将 object 强转为对应的泛型
    return (T) object;
}

三、泛型类

1. 基础介绍

泛型类的定义泛型方法类似,不同之处在于泛型类的作用域是针对于整个类。

下述通过一个简单示例看下泛型类的定义方式,首先当然是新建一个接口类并定义一些常用的增删改方法,这里的类名后多了 <T> ,与上面的的泛型接口相似。

public interface Repository<T> {

    T get(Serializable id);

    T save(T t);

    boolean update(T t);
}

新建一个实现类并实现上面新建的接口类并重写其方法,在此即可定义相同的基本业务处理逻辑,也是当下流行 ORM 工具的实现方式之一。

public abstract class AbstractRepository<T> implements Repository<T> {

    @Override
    public T get(Serializable id) {
        // 根据 ID 获取对象
        return null;
    }

    @Override
    public T save(T t) {
        // 新增对象
        return null;
    }

    @Override
    public boolean update(T t) {
        // 更新对象
        return false;
    }
}

2. 类型获取

相对于泛型接口而言,在泛型类中若需要获取对应的泛型指代类信息则更为困难。因此,在泛型类中获取泛型指代类通常是通过继承方法实现。

下面通过一个示例介绍如果获取泛型指代类信息:

// 定义泛型类
public abstract class AbstractReflect<T> {

    public abstract Class<?> getType();
}

// 继承泛型类
public class StrReflectApi extends AbstractReflect<String> {

    private final Class<?> type;

    @SuppressWarnings("unchecked")
    public StrReflectApi() {
        // Get super class generic class
        ParameterizedType type = (ParameterizedType) getClass().getGenericSuperclass();
        // Get first class type
        this.type = (Class<?>) type.getActualTypeArguments()[0];
    }

    @Override
    public Class<?> getType() {
        return this.type;
    }
}

在上述的示例中,即可通过实例化 StrReflectApi 对象并通过 getType() 方法获取对应的泛型指代类。

如果需要在运行是动态获取 AbstractReflect<T> 类的泛型指代信息,在默认的 JDK 中并不提供该方式。

3. 应用场景

新建一个普通的接口类,并继承之前创建的泛型接口类。

public interface UserRepository extends Repository<User> {

    // 可以在此定义 UserRepository 特有的方法
    void action(User user);

}

新建一个实现类继承 AbstractRepository 并实现 UserRepository 接口,根据继承的传递性,其会同时包含 AbstractRepository 中定义的泛型方法。

public class UserRepositoryImpl extends AbstractRepository<User> 
        implements UserRepository {

}

此时通过实例化 UserRepositoryImpl 对象你可以发现在 UserRepository 并没有定义相关的接口方法却我们却调用相应的增删改方法。

public static void main(String args) {
    UserRepositoryImpl userRepository = new userRepository();
    userRepository.get("123");
    userRepository.save(new User("123", "Alex"));
    userRepository.update(new User("123", "Beth"));
    userRepository.deleted("123");
}

四、泛型边界

在定义泛型方法或泛型类时通常使用 T 指代传入的对象类型,即默认对象为 Object 即可,而通过泛型边界可以限制传入的对象类型。

既然作为边界,理所应当的其提供了上行边界与下行边界,下面将分别进行介绍。

1. 上行边界

泛型的上行边界由 extends 关键字实现,即传入的泛型对象必须继承于指定类。

如下示例中定义的泛型方法 upper1(),通过 <T extends Number> 设定了传入对象必须为 Number 的子类,因此当传入其它对象时将提示非法无法通过编译。

@Test
public void demo1() {
    upper1(123);

    // Illegal, the "String" is not extend from "Number"
    // upper1("hello");
}

private <T extends Number> void upper1(T t) {
    System.out.println("\nBoundary-1: " + t);
}

private <T> void upper2(List<? extends Number> t) {
    System.out.println("\nBoundary-2: " + t);
}

2. 下行边界

同样的下行边界由 super 关键字实现,即传入的泛型对象必须为指定类的超类。

如下示例中定义的 lower1() 方法中指定了泛型 List 容器中对象必须为 Integer 的超类。

@Test
public void demo2() {
    lower1(List.of(123));
}

private void lower1(List<? super Integer> t) {
    System.out.println("\nBoundary-3: " + t);
}

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录