1

Java高级特性-泛型:泛型的基本用法,怎样才能少写 1 万行代码

 1 year ago
source link: https://blog.51cto.com/u_15773567/5736612
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

泛型是 Java 的一个高级特性。在 Mybatis、Hibernate 这种持久化框架,泛型更是无处不在。

然而,泛型毕竟是高级特性,藏在框架的底层代码里面。我们平时都是写业务代码,可能从来没见过泛型,更别提怎么用了。

既然如此,我们就一步步学习泛型吧。

泛型是什么

泛型是一种特殊的类型。你不用一开始就指明参数的具体类型,而是先定义一个类型变量,在使用的时候再确定参数的具体类型。

这好像还是很难理解。没关系,我们先来看看,在没有泛型情况下,我们是怎么做的。

比如,在电商系统中,用户有两种类型,分别是普通用户、商户用户。当用户点击获取信息详情时,系统要先把一些敏感信息设置为空,像是 password 之类字段,然后才返回给用户。

你能写一个通用方法,把这些敏感字段设置为空吗?

你可能想到了,在 Java 中,所有的类都继承了 Object 。于是,你写出了第一个版本。

public class ApplicationV1 {

// 把敏感字段设置为空
public static Object removeField(Object obj) throws Exception {
// 需要过滤的敏感字段
Set<String> fieldSet = new HashSet<String>();
fieldSet.add("password");

// 获取所有字段:然后获取这个类所有字段
Field[] fields = obj.getClass().getDeclaredFields();

// 敏感字段设置为空
for (Field field : fields) {
if (fieldSet.contains(field.getName())) {
// 开放字段操作权限
field.setAccessible(true);
// 设置空
field.set(obj, null);
}
}

// 返回对象
return obj;
}

}

在这个方法中,你把 Object 作为传入参数,然后用反射操作字段,把 password 设置为空。代码一气呵成,于是你又写出了下面的测试代码。

public class ApplicationV1 {

// ...省略部分代码

public static void main(String[] args) throws Exception {
// 初始化
ShopUser shopUser = new ShopUser(0L, "shopUser", "123456");
ClientUser clientUser = new ClientUser(0L, "clientUser", "123456");

// 输出原始信息
System.out.println("过滤前:");
System.out.println(" " + shopUser);
System.out.println(" " + clientUser);

// 执行过滤
shopUser = (ShopUser) removeField(shopUser);
clientUser = (ClientUser) removeField(clientUser);

// 输出过滤后信息
System.out.println("过滤后:");
System.out.println(" " + shopUser);
System.out.println(" " + clientUser);
}
}

运行结果
过滤前:
ShopUser{id=0, username='shopUser', password='123456'}
ClientUser{id=0, username='clientUser', password='123456'}
过滤后:
ShopUser{id=null, username='shopUser', password='null'}
ClientUser{id=null, username='clientUser', password='null'}

运行结果看起来没问题,但很遗憾,这个方法不能用。最显而易见的问题是,简洁性不够。这个方法要强制转换对象,你看看这两行测试代码:

// 执行过滤
shopUser = (ShopUser) removeField(shopUser);
clientUser = (ClientUser) removeField(clientUser);

明明是同一个对象,你过滤掉敏感字段后,自己还得再转换一次对象。你想想看,这好歹是一个通用方法,要用在很多地方,当然是越简单越好。

你又想到了,Java 有方法重载机制,你写出了第二个版本。

public class ApplicationV2 {
/********************** 业务方法 ************************/
public static ShopUser removeField(ShopUser user) throws Exception {
// 强转,并返回对象
return (ShopUser) remove(user);
}
public static ClientUser removeField(ClientUser user) throws Exception {
// 强转,并返回对象
return (ClientUser) remove(user);
}

/********************** 核心方法 ************************/
// 把敏感字段设置为空
public static Object remove(Object obj) throws Exception {
// 需要过滤的敏感字段
Set<String> fieldSet = new HashSet<String>();
fieldSet.add("password");

// 获取所有字段:然后获取这个类所有字段
Field[] fields = obj.getClass().getDeclaredFields();

// 敏感字段设置为空
for (Field field : fields) {
if (fieldSet.contains(field.getName())) {
// 开放字段操作权限
field.setAccessible(true);
// 设置空
field.set(obj, null);
}
}

// 返回对象
return obj;
}
}

这样一来,问题好像又解决了。但新问题来了,重复方法特别多,而且如果再加一个供应商用户,我还得再写一个方法吗?这可是通用方法,动不动就改源码,也不是办法呀。

在没有泛型的情况下,重复代码没法解决,你总得做些没意义的操作。要不强转对象,要不就多写几个方法。

然而, Java 的 1.5 版本引入了泛型机制,代码可以变得更加简单。 利用泛型,你写出了第三个版本。

public class ApplicationV3 {
// 把敏感字段设置为空
public static <T> T removeField(T obj) throws Exception {
// 需要过滤的敏感字段
Set<String> fieldSet = new HashSet<String>();
fieldSet.add("password");

// 获取所有字段:然后获取这个类所有字段
Field[] fields = obj.getClass().getDeclaredFields();

// 敏感字段设置为空
for (Field field : fields) {
if (fieldSet.contains(field.getName())) {
// 开放字段操作权限
field.setAccessible(true);
// 设置空
field.set(obj, null);
}
}

// 返回对象
return obj;
}
}

在第三个版本中,你使用了泛型,调用方法时不用强转对象了,你也不用在源码写这么多重复方法,代码变得更加简单了。

你再仔细看完上面的代码,可以发现, 泛型的使用步骤:定义类型变量  、使用类型变量 T obj 、确定类型变量 removeField(new ShopUser(0L, "shopUser", "123456")) 这点非常重要,这里先按下不表。

这就是泛型,你不用把参数的类型写死在代码,而是在使用的时候,再确定具体的类型。使用了泛型,你的代码可以变得更简单、安全。

当然, 泛型很多的用法,分别是:泛型类及接口、泛型方法、通配符。 接下来,我们就一个个解锁吧~

当泛型用在类和接口时,就被称为泛型类、泛型接口。这个最典型的运用就是各种集合类和接口,比如,List、ArrayList 等等。

那么,我们泛型怎么用在类上面呢?

首先,定义一个泛型类。

public class IdGen<T> {
protected T id;

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

IdGen 是一个 id 生成类。第一行代码中,  是泛型标识,代表你定义了一个类型变量 T。第二行代码,我使用这个类型变量,把 id 定义成一个泛型。

然后,在实例化、继承的的时候,指定具体的类型。

public class IdGen<T> {
// ..省略部分代码

// 通过继承,确定泛型变量
static class User extends IdGen<Integer> {
public User(Integer id) {
super(id);
}
}

public static void main(String[] args) {
// 通过实例化,确定泛型变量
IdGen idGen = new IdGen<String>("1");
System.out.println(idGen);

User user = new User(1);
System.out.println(user);
}
}

用户类继承了 IdGen,在代码 extends IdGen 中,指定了 Integer 作为 id 的具体类型;而 IdGen 实例化的时候,在代码 new IdGen("1") 中,则指定了 String 作为 id 的具体类型。

泛型不仅能用在类和接口上,还可以用在方法上。

比如,怎么把一个类的成员变量转换成 Map 集合呢?

这时候,我们可以写一个泛型方法。

public class Generic {
public static <T> Map obj2Map(T obj) throws Exception {
Map map = new HashMap<>();

// 获取所有字段:通过 getClass() 方法获取 Class 对象,然后获取这个类所有字段
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
// 开放字段操作权限
field.setAccessible(true);
// 设置值
map.put(field.getName(), field.get(obj));
}

return map;
}
}

同样的,  是泛型标识,代表你定义了一个类型变量 T,用在这个方法上。 T obj 使用类型变量 T,定义一个 obj 参数。最后,在调用方法的的时候,再确定具体的类型。

泛型通配符

泛型通配符用 ? 表示,代表不确定的类型,是泛型的一个重要组成。

有一点很多文章都没提到,大家一定要记住!!!

使用泛型有三个步骤:定义类型变量、使用类型变量、确定类型变量。在第三步,确定类型变量的时候,如果你没法明确类型变量,这时候可以用泛型通配符。

一般情况下,我们不需要用到泛型通配符,因为你能明确地知道类型变量,你看下面代码。

public class Application {

public static Integer count(List<Integer> list) {
int total = 0;
for (Integer number : list) {
total += number;
}
list.add(total);
return total;
}

public static void main(String[] args) {
// 不传指定数据,编译报错
List<String> strList = Arrays.asList("0", "1", "2");
int totalNum = count(strList);

// 绕过了编译,运行报错
List strList1 = Arrays.asList("0", "1", "2");
totalNum = count(strList1);
}
}

你非常清楚 count() 方法是干什么的,所以你在写代码的时候,直接就能指明这是一个 Integer 集合。这样一来,在调用方法的时候,如果不传指定的数据进来,就没法通过编译。退一万步讲,即使你绕过了编译这一关,程序也很可能没法运行。

所以,如果你非常清楚自己要干什么,可以很明确地知道类型变量,那没必要用泛型通配符。

然而,在一些通用方法中,什么类型的数据都能传进来,你没法确认类型变量,这时候该怎么办呢?

你可以使用泛型通配符,这样就不用确认类型变量,从而实现一些通用算法。

比如,你要写一个通用方法,把传入的 List 集合输出到控制台,那么就可以这样做。

public class Application {
public static void print(List<?> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}

public static void main(String[] args) {
// Integer 集合,可以运行
List<Integer> intList = Arrays.asList(0, 1, 2);
print(intList);

// String 集合,可以运行
List<String> strList = Arrays.asList("0", "1", "2");
print(strList);
}
}

List<?> list 代表我不确定 List 集合装的是什么类型,有可能是 Integer,有可能是 String,还可能是别的东西。但我不管这些,你只要传一个 List 集合进来,这个方法就能正常运行。

这就是泛型通配符。 此外,有些算法虽然也是通用的,但适用范围不那么大。  比如,用户分为:普通用户、商家用户,但用户有一些特殊功能,其它角色都没有。这时候,又该怎么办呢?

你可以给泛型通配符设定边界,以此限定类型变量的范围。

泛型通配符的上边界

上边界,代表类型变量的范围有限,只能传入某种类型,或者它的子类。你看下这幅图就明白了。

Java高级特性-泛型:泛型的基本用法,怎样才能少写 1 万行代码_泛型

利用 <? extends 类名> 的方式,可以设定泛型通配符的上边界。你看下这个例子就明白了。

public class TopLine {

public static void print(List<? extends Number> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}

public static void main(String[] args) {
// Integer 是 Number 的子类,可以调用 print 方法
print(new ArrayList<Integer>());

// String 不是 Number 的子类,没法调用 print 方法
print(new ArrayList<String>());
}

}

你想调用 print() 方法中,那么你可以传入 Integer 集合,因为 Integer 是 Number 的子类。但 String 不是 Number 的子类,所以你没法传入 String 集合。

泛型通配符的下边界

下边界,代表类型变量的范围有限,只能传入某种类型,或者它的父类。你看下这幅图就明白了。

Java高级特性-泛型:泛型的基本用法,怎样才能少写 1 万行代码_泛型_02

利用 <? super 类名> 的方式,可以设定泛型通配符的上边界。你看下这个例子就明白了。

public class LowLine {

public static void print(List<? super Integer> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}

public static void main(String[] args) {
// Number 是 Integer 的父类,可以调用 print 方法
print(new ArrayList<Number>());

// Long 不是 Integer 的父类,没法调用 print 方法
// print(new ArrayList<String>());
}
}

你想调用 print() 方法中,那么可以传入 Number 集合,因为 Number 是 Integer 的父类。但 Long 不是 Integer 的父类,所以你没法传入 Long 集合。

泛型是一种特殊的类型,你可以把泛型用在类、接口、方法上,从而实现一些通用算法。

此外,使用泛型有三个步骤:定义类型变量、使用类型变量、确定类型变量。

在确定类型变量这一步中,你可以用泛型通配符来限制泛型的范围,从而实现一些特殊算法。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK