运用 Java 8 写一个 通用 Map 转换工具类

Map 是非常常见的一个数据结构,至于多常见则不再赘说了。框架无论大小,都会多少提供 Map 的相关工具方法,或进行封装。笔者在没用使用 Java 8 之前,也封装过,用了一段时间,如今 Java 8 问世几年,是时候对库改造一番了。

库源码:https://gitee.com/sp42_admin/ajaxjs/blob/master/ajaxjs-base/src/main/java/com/ajaxjs/util/MapTool.java

join()

我们知道,String [] 有 join 的方法,把多个 String 转换为字符串,各个元素用 & 联结(或自定义字符),同样我们把该方法延伸到 Map 身上,于是有 join 的方法,

/**
 * Map 转换为 String
 * 
 * @param map Map 结构,Key 必须为 String 类型
 * @param div 分隔符
 * @param fn 对 Value 的处理函数,返回类型 T
 * @return Map 序列化字符串
 */
public static <T> String join(Map<String, T> map, String div, Function<T, String> fn) {
	String[] pairs = new String[map.size()];

	int i = 0;

	for (String key : map.keySet())
		pairs[i++] = key + "=" + fn.apply(map.get(key));

	return String.join(div, pairs);
}

测试:

Map<String, Object> map = new HashMap<String, Object>() {
	private static final long serialVersionUID = 1L;
	{
		put("foo", null);
		put("bar", 500);
		put("zx", "hi");
	}
};

@Test
public void testJoin() {
	assertEquals("bar=500&foo=null&zx=hi", join(as(map, v -> v.toString())));
}

toMap

反之,将字符串转换为 Map,则有 toMap 方法,分别有以下两种情形:

/**
 * String[] 转换为 Map
 * 
 * @param pairs 结对的字符串数组,包含 = 字符分隔 key 和 value
 * @param fn 对 Value 的处理函数,返回类型 Object
 * @return Map 对象
 */
public static Map<String, Object> toMap(String[] pairs, Function<String, Object> fn) {
	if (CommonUtil.isNull(pairs))
		return null;

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

	for (String pair : pairs) {
		if (!pair.contains("="))
			throw new IllegalArgumentException("没有 = 不能转化为 map");

		String[] column = pair.split("=");

		if (column.length >= 2)
			map.put(column[0], fn == null ? column[1] : fn.apply(column[1]));
		else
			map.put(column[0], "");// 没有 等号后面的,那设为空字符串
	}

	return map;
}

/**
 * String[] 转换为 Map,key 与 value 分别一个数组
 * 
 * @param columns 结对的键数组
 * @param values 结对的值数组
 * @param fn 对 Value 的处理函数,返回类型 Object
 * @return Map 对象
 */
public static Map<String, Object> toMap(String[] columns, String[] values, Function<String, Object> fn) {
	if (CommonUtil.isNull(columns))
		return null;

	if (columns.length != values.length)
		throw new UnsupportedOperationException("两个数组 size 不一样");

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

	int i = 0;
	for (String column : columns)
		map.put(column, fn.apply(values[i++]));

	return map;
}

测试:

@Test
public void testToMap() {
	assertEquals(1, MapTool.toMap(new String[] { "a", "b", "d" }, new String[] { "1", "c", "2" }, MappingValue::toJavaValue).get("a"));
	assertEquals(1, MapTool.toMap(new String[] { "a=1", "b=2", "d=c" }, MappingValue::toJavaValue).get("a"));
	assertEquals("你好", MapTool.toMap(new String[] { "a=%e4%bd%a0%e5%a5%bd", "b=2", "d=c" }, Encode::urlDecode).get("a"));
}

值得一提的是,MappingValue::toJavaValue 能把字符串还原为 Java 里面的真实值,如 “true”–true,“123”–123,“null”–null,源码如下,

/**
 * 把字符串还原为 Java 里面的真实值,如 "true"--true,"123"--123,"null"--null
 * 
 * @param value 字符串的值
 * @return Java 里面的值
 */
public static Object toJavaValue(String value) {
	if (value == null)
		return null;

	value = value.trim();

	if ("".equals(value))
		return "";
	if ("null".equals(value))
		return null;

	if ("true".equalsIgnoreCase(value))
		return true;
	if ("false".equalsIgnoreCase(value))
		return false;

	// try 比较耗资源,先检查一下
	if (value.charAt(0) == '-' || (value.charAt(0) >= '0' && value.charAt(0) <= '9'))
		try {
			int int_value = Integer.parseInt(value);
			if ((int_value + "").equals(value)) // 判断为整形
				return int_value;
		} catch (NumberFormatException e) {// 不能转换为数字
		}

	return value;
}

代码比较简单,主要是结合了 Java 8 特性发挥,理解函数可以作为变量“传来传去”就好了。

万能 Map 泛型转换器

为了转换泛型,如 Map<String, Object>Map<String, String> 之间的互转,提供了该方法——说“万能”的口气好像比较大,但扒开源码呢,还是没啥技术含量的,顶多使用了 Function<K, T> fn 函数接口。源码如下,

/**
 * 万能 Map 转换器,为了泛型的转换而设的一个方法,怎么转换在 fn 中处理
 * 
 * @param map 原始 Map,key 必须为 String 类型
 * @param fn 转换函数
 * @return
 */
public static <T, K> Map<String, T> as(Map<String, K> map, Function<K, T> fn) {
	Map<String, T> _map = new HashMap<>();

	for (String key : map.keySet()) {
		K value = map.get(key);
		_map.put(key.toString(), value == null ? null : fn.apply(value));
	}

	return _map;
}

需要注意的是 key 必须为 String 类型。如果不限制,应该也是可以,代码就要复杂一点,当前先不考虑复杂的情况。
测试:

@Test
public void testToMap() {
	assertEquals(1, MapTool.toMap(new String[] { "a", "b", "d" }, new String[] { "1", "c", "2" }, MappingValue::toJavaValue).get("a"));
	assertEquals(1, MapTool.toMap(new String[] { "a=1", "b=2", "d=c" }, MappingValue::toJavaValue).get("a"));
	assertEquals("你好", MapTool.toMap(new String[] { "a=%e4%bd%a0%e5%a5%bd", "b=2", "d=c" }, Encode::urlDecode).get("a"));
}

@Test
public void testAsString() {
	assertEquals("500", as(map, v -> v.toString()).get("bar"));
	assertEquals("[1, c, 2]", as(new HashMap<String, String[]>() {
		private static final long serialVersionUID = 1L;
		{
			put("foo", new String[] { "a", "b" });
			put("bar", new String[] { "1", "c", "2" });
		}
	}, v -> Arrays.toString(v)).get("bar"));
}

Map 与 Bean 的转换

Java Bean 又称 POJO,可以没有任何集成,所以根类是 Object。JDK 自带 Bean “内省”,那样就无须经过反射了。我们先把遍历 bean 各个字段的逻辑抽出来,

扫描二维码关注公众号,回复: 9207437 查看本文章
@FunctionalInterface
public static interface EachFieldArg {
	public void item(String key, Object value, PropertyDescriptor property);
}

/**
 * 遍历一个 Java Bean
 * 
 * @param bean Java Bean
 * @param fn 执行的任务,参数有 key, value, property
 */
public static void eachField(Object bean, EachFieldArg fn) {
	try {
		BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass());

		for (PropertyDescriptor property : beanInfo.getPropertyDescriptors()) {
			String key = property.getName();
			// 得到 property 对应的 getter 方法
			Method getter = property.getReadMethod();
			Object value = getter.invoke(bean);

			fn.item(key, value, property);
		}
	} catch (IntrospectionException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
		LOGGER.warning(e);
	}
}

遍历本身足够简单,唯一亮点是自定义函数接口的使用:@FunctionalInterface。当 JDK 自带的 Supply、Function、Consumer 参数不能满足需求时,自定义函数接口就发挥作用了,例如 public void item(String key, Object value, PropertyDescriptor property); 我们一下子安排了三个参数。

接下来的事情就好办,无法获取值,设置值,交换数据,还有一些细节问题处理就是了。

/**
 * Map 转为 Bean
 * 
 * @param map 原始数据
 * @param clz 实体 bean 的类
 * @param isTransform 是否尝试转换值
 * @return 实体 bean 对象
 */
public static <T> T map2Bean(Map<String, ?> map, Class<T> clz, boolean isTransform) {
	T bean = ReflectUtil.newInstance(clz);

	eachField(bean, (key, v, property) -> {
		try {
			if (map.containsKey(key)) {
				Object value = map.get(key);

				// null 是不会传入 bean 的
				if (value != null) {
					Class<?> t = property.getPropertyType(); // Bean 值的类型,这是期望传入的类型,也就 setter 参数的类型

					if (isTransform && value != null && t != value.getClass()) { // 类型相同,直接传入;类型不相同,开始转换
						value = MappingValue.objectCast(value, t);
					}

					property.getWriteMethod().invoke(bean, value);
				}
			}

			// 子对象
			for (String mKey : map.keySet()) {
				if (mKey.contains(key + '_')) {
					Method getter = property.getReadMethod(), setter = property.getWriteMethod();// 得到对应的 setter 方法

					Object subBean = getter.invoke(bean);
					String subBeanKey = mKey.replaceAll(key + '_', "");

					if (subBean != null) {// 已有子 bean
						if (map.get(mKey) != null) // null 值不用处理
							ReflectUtil.setProperty(subBean, subBeanKey, map.get(mKey));
					} else { // map2bean
						Map<String, Object> subMap = new HashMap<>();
						subMap.put(subBeanKey, map.get(mKey));
						subBean = map2Bean(subMap, setter.getParameterTypes()[0], isTransform);
						setter.invoke(bean, subBean); // 保存新建的 bean
					}
				}
			}
		} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
			LOGGER.warning(e);
		}
	});

	return bean;
}

/**
 * map 转实体
 * 
 * @param map 原始数据
 * @param clz 实体 bean 的类
 * @return 实体 bean 对象
 */
public static <T> T map2Bean(Map<String, ?> map, Class<T> clz) {
	return map2Bean(map, clz, false);
}

/**
 * Bean 转为 Map
 * 
 * @param bean 实体 bean 对象
 * @return Map 对象
 */
public static <T> Map<String, Object> bean2Map(T bean) {
	Map<String, Object> map = new HashMap<>();

	eachField(bean, (k, v, property) -> {
		if (!k.equals("class")) // 过滤 class 属性
			map.put(k, v);
	});

	return map;
}

测试:

public static Map<String, Object> userWithoutChild = new HashMap<String, Object>() {
	private static final long serialVersionUID = 1L;
	{
		put("id", 1L);
		put("name", "Jack");
		put("age", 30);
		put("birthday", new Date());
	}
};

public static class MapMock {
	static boolean s = true;
	public static Map<String, Object> user = new HashMap<String, Object>() {
		private static final long serialVersionUID = 1L;
		{
			put("id", 1L);
			put("name", "Jack");
			put("sex", s);
			put("age", 30);
			put("birthday", new Date());

			put("children", "Tom,Peter");
			put("luckyNumbers", "2, 8, 6");
		}
	};
}

@Test
public void testMap2Bean() {
	TestCaseUserBean user = MapTool.map2Bean(userWithoutChild, TestCaseUserBean.class);// 直接转
	assertNotNull(user);
	assertEquals(user.getName(), "Jack");

	user = MapTool.map2Bean(MapMock.user, TestCaseUserBean.class, true);

	assertNotNull(user);
	assertEquals("Tom", user.getChildren()[0]);
	assertEquals(8, user.getLuckyNumbers()[1]);
	assertEquals(true, user.isSex());
}

@Test
public void testBean2Map() {
	TestCaseUserBean user = MapTool.map2Bean(MapMock.user, TestCaseUserBean.class, true);
	Map<String, Object> map = MapTool.bean2Map(user);

	assertNotNull(map);
	assertEquals("Jack", map.get("name"));
}

XML 与 Bean 互转

XML 部分的代码 copy 第三方代码,没什么好说了。我一向主张使用自带的库,使用直接使用了 Java W3C Dom 解析,小型 XML 文件解析足够了。

发布了293 篇原创文章 · 获赞 260 · 访问量 232万+

猜你喜欢

转载自blog.csdn.net/zhangxin09/article/details/86670936