在项目开发过程中,有时需要将类定义成不可变(Immutable)类型,例如在一些暴露给第三方的接口参数对象,对于复杂多层次的自定义类,手工编写Immutable类是个繁琐且容易出错的工作,为此写了一个Immutable自动生成工具。

1. mutable(可变)和immutable(不可变)类型的区别

可变类型的对象:提供了可以改变其内部数据值的操作,其内部的值可以被重新更改。

不可变数据类型:其内部的操作不会改变内部的值,一旦试图更改其内部值,将会构造一个新的对象而非对原来的值进行更改。

例如Java中String类就是一个Immutable对象

String var = "hello world";

var.toUpperCase();

toUpperCase()方法不会改变var中包含的数据“Hello word”。而是创建一个新的String对象并将其初始化为“HELLO WORLD”,然后返回这个新对象的引用。

2.mutable和immutable类型的优缺点

mutable 优点:减少数据的拷贝次数,从而其效率 要高于immutable

缺点:可变类型由于其内部数据可变,所以其风险更大

immutable 缺点: 由于内部数据不可变,所以对其频发修改会产生大量的临时拷贝,浪费空间。

优点:内部数据的不可变导致其更加安全,可以用作多线程的共享对象而不必考虑同步问题

3.如何构造一个immutable类

用private final修饰所有fileds中的成员;private保证内部成员不会被外部直接访问;final确保在成员被初始化之后不会被重新assigned

不提供改变成员的方法如setter

使用final修饰自定义类,确保类中的所有方法不会被重写。

4.复杂自定义类Immutable生成器

简单的类,可以通过第3节方法手动编写,但如果field中有大量自定义类,这些自定义类还包含很多自定义类filed,那么手工编写Immutable将会变得非常繁琐,会充斥大量的get方法以及赋值操作,很容易出错,另外与Array、List、Map等类型时,对象的深度拷贝都需要特殊处理。为此写一个Imuutable类的自动生成工具,具体代码如下:

package com.wwb.utils;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import com.google.common.base.Function;
/**

* 不可变类(Immutable)生成工具类 功能: 1.支持原生类型、Date、BigDecimal、Map、Set、List

* 2.支持自定义类自动生成不可变类 3.支持集合类生成真正不可变集合

*

* 限制: 1.不支持不同包下的同名类 2.不支持多层参数集合类,例如List>,仅支持一层

*

* 使用方法: 1.ImmutableClassTool tool = new ImmutableClassTool();
* 2.tool.addIngoreClass(xxx.class);//指定忽略的类型
* 3.tool.addIngoreProperty("zzz");//指定忽略的属性名 4.String classStr =
* tool.generate(CustomizeClass.class);
*
* @author wwb 2019-1-19
*/
public class ImmutableClassTool {
private static final String CLASS_PREFIX = "Immutable";
private static final String LINE_BREAK = "\n";
private Set unGeneratedClass = new HashSet();
private Set generatedClass = new HashSet();
private Set packageSet = new HashSet();
private Set> ignoreClass = new HashSet>();// 忽略的类
private Set ignoreProperty = new HashSet();// 忽略的属性名
public void addIngoreClass(Class> cls) {
ignoreClass.add(cls);
}
public void addIngoreProperty(String propertyName) {
ignoreProperty.add(propertyName);
}
private String generataImmutaleClass(Class> cls) throws ClassNotFoundException, IntrospectionException {
if (generatedClass.contains(cls.getName())) {
return LINE_BREAK;
}
StringBuilder builder = new StringBuilder();
String srcClassName = cls.getSimpleName();
String className = CLASS_PREFIX + srcClassName;
builder.append("\n@Getter\n");
builder.append("public final class ");
builder.append(className).append(" {\n");
/* generate all fileds */
builder.append(this.generateFields(cls));
/* generate constructor */
builder.append(this.generateConstructor(cls));
builder.append(" }\n");
generatedClass.add(cls.getName());
unGeneratedClass.remove(cls.getName());
return builder.toString();
}
public String generate(Class> cls) throws ClassNotFoundException, IntrospectionException {
StringBuilder builder = new StringBuilder();
String srcClassName = cls.getSimpleName();
String className = CLASS_PREFIX + srcClassName;
builder.append("\n@Getter\n");
builder.append("public final class ");
builder.append(className).append(" {\n");
builder.append(generateFields(cls));
builder.append(generateConstructor(cls));
builder.append(generateInnerImmutableClass());
builder.insert(0, generateComment(cls));
builder.insert(0, generatePackageExpr(cls));
builder.append("}\n");
return builder.toString();
}
/**
* generate fields
*
* @param cls
* @return
*/
private String generateFields(Class> cls) {
/* generate all fileds */
StringBuilder builder = new StringBuilder();
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
if (hasGetMethod(field, cls)) {
builder.append(" private final ");
builder.append(getFieldType(field));
builder.append(" ");
builder.append(field.getName()).append(";\n");
}
}
return builder.toString();
}
/**
* generate constructor
*
* @param cls
* @return
* @throws IntrospectionException
*/
private String generateConstructor(Class> cls) throws IntrospectionException {
StringBuilder builder = new StringBuilder();
String srcClassName = cls.getSimpleName();
String className = CLASS_PREFIX + srcClassName;
Field[] fields = cls.getDeclaredFields();
String paramName = srcClassName.substring(0, 1).toLowerCase() + srcClassName.substring(1);
builder.append(" public ").append(className).append("(").append(srcClassName).append(" ").append(paramName)
.append("){\n");
builder.append(" Assert.notNull(").append(paramName).append(",\"").append(paramName).append(" is null\");\n");
this.packageSet.add("org.springframework.util.Assert");
for (Field filed : fields) {
if (hasGetMethod(filed, cls)) {
builder.append(" this.").append(filed.getName()).append(" = ");
builder.append(generateGetExpr(filed, paramName, cls)).append(";\n");
}
}
builder.append(" }\n");
return builder.toString();
}
private String generateInnerImmutableClass() throws ClassNotFoundException, IntrospectionException {
/* generate immutable class */
StringBuilder builder = new StringBuilder();
while (unGeneratedClass.size() > 0) {
HashSet unGeneratedClassCopy = new HashSet();
unGeneratedClassCopy.addAll(unGeneratedClass);
for (String clsName : unGeneratedClassCopy) {
builder.append(generataImmutaleClass(Class.forName(clsName)));
}
}
return builder.toString();
}
private boolean isImmutable(Class> type) {
if (ClassUtils.isPrimitiveOrWrapper(type) || type.isAssignableFrom(String.class)
|| type.isAssignableFrom(BigDecimal.class) || type.isAssignableFrom(Date.class)
|| type.isAssignableFrom(Map.class) || type.isAssignableFrom(List.class)
|| type.isAssignableFrom(Set.class)) {
return true;
} else if (type.isArray() && isImmutable(type.getComponentType())) {
return true;
} else if (this.ignoreClass.contains(type)) {
return true;
}
return false;
}
private boolean hasGetMethod(Field filed, Class> cls) {
String filedName = filed.getName();
try {
PropertyDescriptor descriptor = new PropertyDescriptor(filedName, cls);
return descriptor.getReadMethod() != null;
} catch (IntrospectionException e) {
return false;
}
}
private String generateGetExpr(Field field, String paramName, Class> cls) throws IntrospectionException {
StringBuilder builder = new StringBuilder();
String filedName = field.getName();
Class> type = field.getType();
String getMethodExpr = null;
PropertyDescriptor descriptor = new PropertyDescriptor(filedName, cls);
Method readMethod = descriptor.getReadMethod();
if (readMethod != null) {
getMethodExpr = paramName + "." + readMethod.getName() + "()";
}
if (ClassUtils.isPrimitiveOrWrapper(type) || type.isAssignableFrom(String.class)
|| type.isAssignableFrom(BigDecimal.class) || this.ignoreProperty.contains(field.getName())) {
builder.append(getMethodExpr);
} else if (type.isAssignableFrom(Date.class)) {
builder.append("ImmutableClassTool.clone(").append(getMethodExpr).append(")");
} else if (type.isArray()) {
builder.append(getArrayTransformMethod(type, getMethodExpr));
} else if (type.isAssignableFrom(Map.class)) {
builder.append("ImmutableMap.copyOf(");
Type genericType = field.getGenericType();
if (genericType instanceof ParameterizedType) {
Class> paramClass = (Class>) ((ParameterizedType) genericType).getActualTypeArguments()[1];
if (isImmutable(paramClass)) {
builder.append(getMethodExpr);
} else {
builder.append(getGuavaTransformMethod(paramClass, getMethodExpr, Map.class));
}
}
builder.append(")");
this.packageSet.add("com.google.common.collect.ImmutableMap");
} else if (type.isAssignableFrom(List.class)) {
builder.append("ImmutableList.copyOf(");
Type genericType = field.getGenericType();
if (genericType instanceof ParameterizedType) {
Class> paramClass = (Class>) ((ParameterizedType) genericType).getActualTypeArguments()[0];
if (isImmutable(paramClass)) {
builder.append(getMethodExpr);
} else {
builder.append(getGuavaTransformMethod(paramClass, getMethodExpr, List.class));
}
}
builder.append(")");
this.packageSet.add("com.google.common.collect.ImmutableList");
} else if (type.isAssignableFrom(Set.class)) {
builder.append("ImmutableSet.copyOf(");
Type genericType = field.getGenericType();
if (genericType instanceof ParameterizedType) {
Class> paramClass = (Class>) ((ParameterizedType) genericType).getActualTypeArguments()[0];
if (isImmutable(paramClass)) {
builder.append(getMethodExpr);
} else {
builder.append(getGuavaTransformMethod(paramClass, getMethodExpr, Set.class));
}
}
builder.append(")");
this.packageSet.add("com.google.common.collect.ImmutableSet");
} else {
builder.append(getMethodExpr);
builder.append(" == null ? null :");
builder.append(" new ");
builder.append(CLASS_PREFIX);
builder.append(field.getType().getSimpleName());
builder.append("(").append(getMethodExpr).append(")");
}
return builder.toString();
}
private String generatePackageExpr(Class> cls) {
StringBuilder builder = new StringBuilder();
builder.append("package ").append(cls.getPackage().getName()).append(";\n\n");
for (String pack : this.packageSet) {
builder.append("import ").append(pack).append(";\n");
}
builder.append("import lombok.Getter;\n");
return builder.toString();
}
private String getFieldType(Field field) {
Class> type = field.getType();
if (ClassUtils.isPrimitiveOrWrapper(type)) {
return type.getSimpleName();
} else if (type.isAssignableFrom(String.class) || type.isAssignableFrom(BigDecimal.class)
|| type.isAssignableFrom(Date.class)) {
this.packageSet.add(type.getName());
return type.getSimpleName();
} else if (type.isAssignableFrom(Map.class) || type.isAssignableFrom(List.class)
|| type.isAssignableFrom(Set.class)) {
this.packageSet.add(type.getName());
Type genericType = field.getGenericType();
if (genericType instanceof ParameterizedType) {
Type[] actualTypeArguments = ((ParameterizedType) genericType).getActualTypeArguments();
if (type.isAssignableFrom(Map.class)) {
Class> keyClass = (Class>) actualTypeArguments[0];
Class> valueClass = (Class>) actualTypeArguments[1];
return "Map";
}
if (type.isAssignableFrom(List.class)) {
return "List) actualTypeArguments[0]) + ">";
}
if (type.isAssignableFrom(Set.class)) {
return "Set) actualTypeArguments[0]) + ">";
}
}
return genericType.toString();
} else if (this.ignoreProperty.contains(field.getName())) {
this.packageSet.add(type.getName());
return type.getSimpleName();
} else {
return getParameterClass(type);
}
}
private String getParameterClass(Class> parameterClass) {
if (isImmutable(parameterClass)) {
return parameterClass.getSimpleName();
} else {
String clsName = parameterClass.isArray() ? parameterClass.getComponentType().getName()
: parameterClass.getName();
this.unGeneratedClass.add(clsName);
this.packageSet.add(clsName);
return CLASS_PREFIX + parameterClass.getSimpleName();
}
}
private String getGuavaTransformMethod(Class> paramClass, String getMethodExpr, Class> collectionType) {
StringBuilder builder = new StringBuilder();
if (collectionType.equals(List.class)) {
builder.append("ImmutableClassTool.transformList(");
} else if (collectionType.equals(Set.class)) {
builder.append("ImmutableClassTool.transformSet(");
} else if (collectionType.equals(Map.class)) {
builder.append("ImmutableClassTool.transformMapValues(");
} else {
return "";
}
builder.append(getMethodExpr);
builder.append(", new Function
builder.append(paramClass.getSimpleName());
builder.append(",");
builder.append(getParameterClass(paramClass));
builder.append(">(){\n @Override\n public " + getParameterClass(paramClass) + " apply("
+ paramClass.getSimpleName() + " input) {\n return new " + getParameterClass(paramClass)
+ "(input);\n }})");
this.packageSet.add("com.google.common.base.Function");
this.packageSet.add(this.getClass().getName());
this.packageSet.add(paramClass.getName());
return builder.toString();
}
private String getArrayTransformMethod(Class> paramClass, String getMethodExpr) {
if (paramClass.isArray()) {
Class> elementType = paramClass.getComponentType();
if (!this.isImmutable(elementType)) {
StringBuilder builder = new StringBuilder();
builder.append("ImmutableClassTool.transformArray(");
builder.append(getMethodExpr);
builder.append(",");
builder.append(getParameterClass(elementType));
builder.append(".class, new Function
builder.append(elementType.getSimpleName());
builder.append(",");
builder.append(getParameterClass(elementType));
builder.append(">(){\n @Override\n public " + getParameterClass(elementType) + " apply("
+ elementType.getSimpleName() + " input) {\n return new " + getParameterClass(elementType)
+ "(input);\n }})");
this.packageSet.add("com.google.common.base.Function");
this.packageSet.add(elementType.getName());
this.packageSet.add(this.getClass().getName());
return builder.toString();
}
}
return getMethodExpr;
}
private String generateComment(Class> cls) {
StringBuilder builder = new StringBuilder();
builder.append("\n/**\n * Immutable class of ");
builder.append(cls.getName());
builder.append("\n * generated by ImmutableClassTool\n */");
return builder.toString();
}
@SuppressWarnings("unchecked")
public static T[] transformArray(F[] fromArray, Class type, Function super F, ? extends T> function) {
if (ObjectUtils.isEmpty(fromArray)) {
return null;
}
T[] toArray = (T[]) Array.newInstance(type, fromArray.length);
for (int i = 0; i < fromArray.length; i++) {
toArray[i] = function.apply(fromArray[i]);
}
return toArray;
}
public static List transformList(List fromList, Function super F, ? extends T> function) {
if (fromList == null) {
return Collections.emptyList();
}
List list = (fromList instanceof LinkedList) ? new LinkedList() : new ArrayList(fromList.size());
for (F f : fromList) {
list.add(function.apply(f));
}
return list;
}
public static Set transformSet(Set fromSet, Function super F, ? extends T> function) {
if (fromSet == null) {
return Collections.emptySet();
}
Set set = (fromSet instanceof TreeSet) ? new TreeSet() : new HashSet(fromSet.size());
for (F f : fromSet) {
set.add(function.apply(f));
}
return set;
}
public static Map transformMapValues(Map fromMap, Function super V1, V2> function) {
if(fromMap == null){
return Collections.emptyMap();
}
Map map = (fromMap instanceof TreeMap) ? new TreeMap() : new HashMap();
for (Entry entry : fromMap.entrySet()) {
map.put(entry.getKey(), function.apply(entry.getValue()));
}
return map;
}
public static Date clone(Date date) {
return date != null ? (Date) date.clone() : null;
}
}