最近在做项目过程中多次遇到该问题,所以整理一下做个笔记。

该错误出现的原因一般都是因为不停的循环递归调用。
1、虚拟机栈是什么?
栈也叫栈内存,是java虚拟机的内存模型之一。它的生命周期是在线程创建时创建,线程结束而消亡,释放内存。因此是私有的,不可共享
栈存储的数据,以栈帧(Stack Frame)为单位存储,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法执行完毕后,F2栈帧先出栈,F1栈帧再出栈,遵循“先进后出”原则。
2、栈帧:每当一个java方法被执行时都会在虚拟机中新创建一个栈帧,方法调用结束后即被销毁
栈帧存储数据包含以下5个部分:
①.局部变量表 :保存函数的参数以及局部变量用的,局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,随着函数栈帧的销毁,局部变量表也会随之销毁。
存放基本数据类型变量(boolean、byte、char、short、int、float)、引用类型的变量(reference)、returnAddress(指向一条字节码指令的地址)类型的变量。
②.操作数栈:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。只支持出栈入栈操作。在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传递。
③.动态链接
④.方法出口信息
⑤.其他

所以当方法中递归调用出现死循环时会抛出该异常。
当对象转成String过程中,如果对象出现互相引用的情况,如果没有做好处理就会出现java.lang.StackOverflowError。
 
public class Student {
    private int id;
    private String name;
    private List<Teacher> teachers = new ArrayList<Teacher>();
    //get、set等方法省略
}
public class Teacher {
    private int id;
    private String name;
    private List<Student> students = new ArrayList<Student>();
    //get、set等方法省略
}
Student类里有个List集合存放Teacher对象,Teacher类中也有个List集合存放Studen对象。
 
public class Test {
    public static void main(String[] args) throws Exception{
        Student student = new Student();
        student.setId(111);
        student.setName("张三");
        List<Student> students = new ArrayList<Student>();
        students.add(student);
        Teacher teacher = new Teacher();
        teacher.setId(100);
        teacher.setName("李四");
        List<Teacher> teachers = new ArrayList<Teacher>();
        teachers.add(teacher);
        teacher.setStudents(students);
        student.setTeachers(teachers);
        ObjectAnalyzer analyzer = new ObjectAnalyzer();
        System.out.println("方式一:");
        System.out.println(analyzer.objToString(student));
        System.out.println("方式二:");
        System.out.println(JSON.toJSONString(student));
        System.out.println("方式三:");
        System.out.println(new Gson().toJson(student));
        
    }
}
studen对象的list集合中包含了teacher,teacher对象的list集合中包含了studen。然后我使用了三种方式来将student对象转换成String。 三种方式分别是:1.通用的对象转String方法,参考于《Java核心技术》一书,就是遍历对象的所有属性,如果属性是对象递归遍历,直到属性是基本类型就转成String;2.使用阿里巴巴的 fastjson提供的toJSONString()方法,版本1.2.38;3.使用google的Gson提供的toJson()方法,版本号2.8.0;

三种方式执行结果:
 
方式一:
com.bawy.study.jdk.string.Student[id=111,name=张三,teachers=java.util.ArrayList[elementData=class java.lang.Object[]{com.bawy.study.jdk.string.Teacher[id=100,name=李四,students=java.util.ArrayList[elementData=class java.lang.Object[]{...,null,null,null,null,null,null,null,null,null},size=1][modCount=1][][]][],null,null,null,null,null,null,null,null,null},size=1][modCount=1][][]][]
方式二:
{"id":111,"name":"张三","teachers":[{"id":100,"name":"李四","students":[{"$ref":"$"}]}]}
方式三:
Exception in thread "main" java.lang.StackOverflowError
    at java.io.StringWriter.write(StringWriter.java:112)
    at com.google.gson.stream.JsonWriter.string(JsonWriter.java:591)
    at com.google.gson.stream.JsonWriter.writeDeferredName(JsonWriter.java:402)
    at com.google.gson.stream.JsonWriter.value(JsonWriter.java:527)
    at com.google.gson.internal.bind.TypeAdapters$7.write(TypeAdapters.java:250)
    at com.google.gson.internal.bind.TypeAdapters$7.write(TypeAdapters.java:235)
    ...
前两种方式均可以实现转换,第三种方式出现 java.lang.StackOverflowError。据说Gson也可以避免出现死循环,具体没有研究。

方式一转换后信息比较全,方式二比较简洁,但是方法二的缺点是如果属性没有get方法,那么在转成字符串后是不会有该属性的值的。
可以看到方式一中teaches集合中的studen对象变成了“...”,方式二中变成了'{"$ref":"$"}',下面贴一下方式一的代码,就可以知道其中原因。
 
public class ObjectAnalyzer {
    private ArrayList<Object> visited = new ArrayList<Object>();
    public String objToString(Object obj){
        if (obj==null){
            return "null";
        }
        //避免出现因为互相引用而出现死循环
        if (visited.contains(obj)){
            return "...";
        }
        visited.add(obj);
        Class clz = obj.getClass();
        if (clz==String.class){
            return (String) obj;
        }
        if (clz.isArray()){
            String r = clz.getComponentType()+"[]{";
            for (int i = 0; i< Array.getLength(obj); i++){
                if (i>0){
                    r += ",";
                }
                Object val = Array.get(obj,i);
                if (clz.getComponentType().isPrimitive()){
                    r +=val;
                }else{
                    r += objToString(val);
                }
            }
            return r+"}";
        }
        String r = clz.getName();
        do{
            r = r+"[";
            Field[] fields = clz.getDeclaredFields();
            AccessibleObject.setAccessible(fields,true);
            for (Field f:fields){
                int modifier = f.getModifiers();
                if (!Modifier.isStatic(modifier)){
                    if (!r.endsWith("[")){
                        r = r+",";
                    }
                    r +=f.getName()+"=";
                    try {
                        Class t = f.getType();
                        Object val = f.get(obj);
                        if (t.isPrimitive()){
                            r +=val;
                        }else{
                            r +=objToString(val);
                        }
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
            r += "]";
            clz = clz.getSuperclass();
        }while (clz!=null);
        return r;
    }
}
可以看到类ObjectAnalyzer中有一个集合visited,用来保存已经访问过的对象,即已经转换成String的对象,再次遇到该对象的时候不再访问对象的所有属性,而是直接返回“...”,以此避免死循环出现。fastJson大概也是进行类似处理,当遇到已经访问过的对象直接返回‘{"$ref":"$"}’。

方法一也有一个缺陷,假如多个对象的属性一模一样,转成String后信息不全。
 
public static void main(String[] args) throws Exception{
        List<Student> students = new ArrayList<Student>();
        Student student = new Student();
        student.setId(111);
        student.setName("张三");
        students.add(student);
        Student student2 = new Student();
        student2.setId(111);
        student2.setName("张三");
        students.add(student2);
        ObjectAnalyzer analyzer = new ObjectAnalyzer();
        System.out.println("方式一:");
        System.out.println(analyzer.objToString(students));
        System.out.println("方式二:");
        System.out.println(JSON.toJSONString(students));
}
结果如下:
 
方式一:
java.util.ArrayList[elementData=class java.lang.Object[]{com.bawy.study.jdk.string.Student[id=111,name=张三,teachers=java.util.ArrayList[elementData=class java.lang.Object[]{},size=0][modCount=0][][]][],...,null,null,null,null,null,null,null,null},size=2][modCount=2][][]
方式二:
[{"id":111,"name":"张三","teachers":[]},{"id":111,"name":"张三","teachers":[]}]
方式一中结合的第二个值变为了“...”,方式二是全的。因为使用了ArrayLsit自己的contains方法,该方法最后会调用对象本身的equals方法和集合中的所有对象进行比较,一般重写的equals方法就是比对所有属性是否相等,所以上述两个对象会被认为是同一个对象,导致二个对象不再访问。
 
if (visited.contains(obj)){
    return "...";
}
建议判断的时候可以改成如下形式,直接使用“==”判断两个对象是否相等。
 
private boolean contain(List<Object> list, Object object){
    for (Object obj:list){
        if (obj==object){
            return true;
        }
    }
    return false;
}

当然对于这种互相引用的情况java也提供了关键字 transient 。使用该关键字修饰属性teachers。
 
private transient List<Teacher> teachers = new ArrayList<Teacher>();
再看三种情况结果:
 
方式一:
com.bawy.study.jdk.string.Student[id=111,name=张三,teachers=java.util.ArrayList[elementData=class java.lang.Object[]{com.bawy.study.jdk.string.Teacher[id=100,name=李四,students=java.util.ArrayList[elementData=class java.lang.Object[]{...,null,null,null,null,null,null,null,null,null},size=1][modCount=1][][]][],null,null,null,null,null,null,null,null,null},size=1][modCount=1][][]][]
方式二:
{"id":111,"name":"张三"}
方式三:
{"id":111,"name":"张三"}
可以看到使用关键字修饰后,三种方式均可以进行转换了。方式二和方式三中teachers属性直接没有了。方法一中也可以添加处理过滤被transient修饰的属性。transient的用途在于:阻止实例中那些用此关键字声明的变量持久化,所以加上该关键字后对应的属性序列化的时候会被忽略。


Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐