Java泛型设计教程


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

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

一、定义概念

1. 泛型擦除

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

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

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

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

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

如上述的代码示例 convert() 部分方法在编译后的 class 中相对应的代码格式如下,即会替换为实际生效的 Java 类。

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;
    }
}

完成上述步骤基础内容定义之后,新建具体的实现类继承刚才定义的 AbstractRepository,可以看到类中无需定义任何内容,其将继承父类中已定义的增删改实现。

如此一来,我们便实现了基础方法的统一抽象实现,使得代码更为简洁紧凑。

public class UserRepository extends AbstractRepository<User> {

}


// 实例化示例
public class MyTest {
    public static void main(String args) {
        Repository repository = new UserRepository();
        repository.save(new User("123", "Alex"));
        userRepository.get("123");
        repository.update(new User("123", "Beth"));
    }
}

2. 类型获取

相对于泛型接口而言,在泛型类中若需要获取对应的泛型指代类信息则更为困难。

由于泛型是在运行时转化获取具体的类型,因此在开发时并无法获取泛型类的具体类型。以下述代码为例,无法实现在当前类获取具体的 T 指代的 Class 对象类型。

public class AbstractReflect<T> {

    public Class<?> getType() {
        // 获取当前 T 的类型
        
    }
}

那倘若需要实现此功能又该如何实现呢?方法其实也并不难,通常采用采用继承的方法实现。

下面通过示例演示如何获取泛型指代类信息,在类的构造函数中获取父类的类型,也就是指代的 T。同时,因为只定义的一个泛型参数,因此只取数组的第一个元素。

public class AbstractReflect<T> {
    
    protected Class<?> type;

    @SuppressWarnings("unchecked")
    public AbstractReflect() {
        // 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;
    }
}

看到这你或许会疑惑,这不是和刚才的提到相互矛盾类,泛型类无法获取自身类型吗?

是的,这个是没错但让我们继续往下看。

新建子类 StringReflectApi 并继承与 AbstractReflect,注意此时 AbstractReflect 参数中已经为具体的类而非 T 指代了。同时,定义类构造函数调用父类的构造器,也就是上述所定义类型获取实现。

在此时当执行上述的构造器中的 getClass() 返回实例将为 StringReflectApi 而非 AbstractReflect,因此在执行 type.getActualTypeArguments()[0] 返回的将为下述中定义的 String.class

经过我们一顿猛如虎的操作,便巧妙的利用继承实现的泛型类的类型获取。

public class StringReflectApi extends AbstractReflect<String> {

    public StringReflectApi() {
        super();
    }
}

public class MyTest {
    public static void main(String[] args) {
        AbstractReflect api = new StringReflectApi();
        api.getType();
    }
}

四、泛型边界

在定义泛型方法或泛型类时通常使用 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 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录