Skip to content

项目中遇到的问题

zhpanvip edited this page Mar 6, 2021 · 3 revisions

一、一个非静态内部类引起的空指针

这是前段时间同事在项目中碰到的一个问题,由非静态内部类引起的一个空指针。

1.问题复现

现在有一个Book类,嵌套了一个非静态内部类Picture,结构如下:

public class Book {

    private String bookName;
    private Picture picture;

    public String getBookName() {
        return bookName;
    }

    public Picture getPicture() {
        return picture;
    }

    public class Picture {
        private String pictureName;

        public String getPictureName() {
            return pictureName;
        }

        public String getBookName() {
            return bookName;
        }
    }
}
{
  bookName:"Nice Book",
  picture:{
    "pictureName":"Nice Picture"
  }
}

有如上述Json数据,将其通过Gson解析成Book,代码如下:

public static void main(String[] args) {
    Gson gson = new Gson();
    String jsonStr = "{\n" +
            "  bookName:\"Nice Book\",\n" +
            "  picture:{\n" +
            "    \"pictureName\":\"Nice Picture\"\n" +
            "  }\n" +
            "}";
    Book book = gson.fromJson(jsonStr, Book.class);
    System.out.println(book.getBookName() + "  " + book.getPicture().getPictureName());
    }

上述代码运行正常,打印结果:

Nice Book Nice Picture

接下来将book.getPicture().getPictureName()换成book.getPicture().getBookName()再次运行,程序出现空指针异常:

Exception in thread "main" java.lang.NullPointerException
	at Book$Picture.getBookName(Book.java:22)
	at Test.main(Test.java:13)

2.NullPointerException异常分析

首先来看这个空指针应该是哪里抛出来的,首先book肯定不为null,而book.getPicture()这段代码在getPictureName()时候可以正常运行,说明Book实例中的picture实例也不为null,那么出现问题的一定是在getBookName()这句代码,而看getBookName的代码似乎也没有任何可能引起空指针的情况:

   public String getBookName() {
       return bookName;
   }

其实,回想一下大家应该都知道“非静态内部类默认持有外部类的引用”这句话。但是很多人并不知道其原理。我们将Book$Picture.class字节码文件反编译得到如下:

public class Book$Picture {
    private String pictureName;

    public Book$Picture(Book var1) {
        this.this$0 = var1;
    }

    public String getPictureName() {
        return this.pictureName;
    }

    public String getBookName() {
        return this.this$0.bookName;
    }
}

可以看到在getBookName中调用的是this.this$0.bookName,而这个崩溃的原因就出现在这个this$0上,即this$0即为外部类Book的引用,此时this$0为null,引发了空指针。

有些同学可能注意到反编译后的代码,Picture多了一个Book参数的构造方法,但是在Picture的源代码中并没有添加任何构造方法。这个构造方法中Book赋值给了this$0,而this$0的角色更像是Picture的一个成员变量,只不过反编译的代码中并没有体现出来。其实到这里已经可以解释空指针出现的原因了,是因为Gson在初始化时候没有传入Book的实例,导致的。

但是,不知道你会不会好奇,Gson是怎么实例化Picture类的呢?是通过反射调用Picture的默认的构造方法?或者说是通过反射调用Picture的有参构造方法,然后Book参数传入了null?

不妨先通过反射试验一下:

    Class<Book.Picture> pictureClass = Book.Picture.class;
    try {
        Book.Picture picture = pictureClass.newInstance();
    } catch (InstantiationException | IllegalAccessException e) {
        e.printStackTrace();
    }

运行结果出现了崩溃:

java.lang.InstantiationException: Book$Picture
	at java.base/java.lang.Class.newInstance(Class.java:571)
	at Test.main(Test.java:20)
Caused by: java.lang.NoSuchMethodException: Book$Picture.<init>()
	at java.base/java.lang.Class.getConstructor0(Class.java:3349)
	at java.base/java.lang.Class.newInstance(Class.java:556)
	... 1 more

可见编译器其实并没有生成无参构造方法。那来尝试一下调用Picture的有有参构造:

        Class<Book.Picture> pictureClass = Book.Picture.class;
        try {
            Book book = new Book();
            Constructor<Book.Picture> declaredConstructor = pictureClass.getDeclaredConstructor(Book.class);
            Book.Picture picture = declaredConstructor.newInstance(book);
            System.out.println(picture.getBookName());
        } catch (InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
            e.printStackTrace();
        }

此时,出现了跟开始时候一样的NullPointerException

Exception in thread "main" java.lang.NullPointerException
	at Book$Picture.getBookName(Book.java:22)
	at Test.main(Test.java:23)

虽然,我们通过反射传入null的方式复现除了项目中的异常,但是就代表Gson是通过反射传入null的方式解析的吗?这道真未必,接下俩看下Gosn的源码

3.Gson是如何实例化非静态内部类的?

通过追踪Gson的fromJson方法,最终看到了如下代码:

    public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
        Object object = this.fromJson((String)json, (Type)classOfT);
        return Primitives.wrap(classOfT).cast(object);
    }

    public <T> T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, JsonSyntaxException {
        boolean isEmpty = true;
        boolean oldLenient = reader.isLenient();
        reader.setLenient(true);

        AssertionError error;
        try {
            try {
                reader.peek();
                isEmpty = false;
                TypeToken<T> typeToken = TypeToken.get(typeOfT);
                // 根据TypeToke获取TypeAdapter
                TypeAdapter<T> typeAdapter = this.getAdapter(typeToken);
                T object = typeAdapter.read(reader);
                Object var8 = object;
                return var8;
            } catch (EOFException var15) {
                // ...
        } finally {
            reader.setLenient(oldLenient);
        }

        return error;
    }

上述代码中核心是通过TypeToken获取到TypeAdapter,在这里TypeAdapter对应的是TypeAdapterFactory.create()出来的Adapter。TypeAdapterFactory是一个接口,在Gson中有众多类实现它的工厂类,而在我们当前场景下对应的是ReflectiveTypeAdapterFactory。那接下来看下ReflectiveTypeAdapterFactory的create方法:

public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        Class<? super T> raw = type.getRawType();
        if (!Object.class.isAssignableFrom(raw)) {
            return null;
        } else {
            // 此处get为核心代码
            ObjectConstructor<T> constructor = this.constructorConstructor.get(type);
            return new ReflectiveTypeAdapterFactory.Adapter(constructor, this.getBoundFields(gson, type, raw));
        }
    }

create方法中调用了constructorConstructor.get方法,跟进来看:

public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
        final Type type = typeToken.getType();
        Class<? super T> rawType = typeToken.getRawType();

         // ... 省略从缓存中取ObjectConstructor代码

         // 这个方法中会通过反射调尝试用类的的无参构造方法
         ObjectConstructor<T> defaultConstructor = this.newDefaultConstructor(rawType);
         if (defaultConstructor != null) {
             return defaultConstructor;
         } else {  // 调用无参构造方法失败
             // 这个方法里面都是一些集合类相关对象的逻辑
             ObjectConstructor<T> defaultImplementation = this.newDefaultImplementationConstructor(type, rawType);
             return defaultImplementation != null ? defaultImplementation : this.newUnsafeAllocator(type, rawType);
         }
    }

从上述代码中可以分析,通过反射调用无参构造方法失败后则会通过newUnsafeAllocator来实例化对象:

    private <T> ObjectConstructor<T> newUnsafeAllocator(final Type type, final Class<? super T> rawType) {
        return new ObjectConstructor<T>() {
            private final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();

            public T construct() {
                try {
                    Object newInstance = this.unsafeAllocator.newInstance(rawType);
                    return newInstance;
                } catch (Exception var2) {
                    throw new RuntimeException("Unable to invoke no-args constructor for " + type + ". Registering an InstanceCreator with Gson for this type may fix this problem.", var2);
                }
            }
        };
    }

通过UnsafeAllocator初始化对象,这个类也是Gson中提供的一个类,用于不安全的实例化对象,源码如下:

// UnsafeAllocator

public abstract <T> T newInstance(Class<T> var1) throws Exception;

public static UnsafeAllocator create() {
        try {
            Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
            Field f = unsafeClass.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            final Object unsafe = f.get((Object)null);
            final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
            return new UnsafeAllocator() {
                public <T> T newInstance(Class<T> c) throws Exception {
                    assertInstantiable(c);
                    return allocateInstance.invoke(unsafe, c);
                }
            };
        } catch (Exception var6) {
            // ...省略异常处理
        }
    }

可以看到这里是通过Java提供的Unsafe这个类来实例化出的一个不安全对象。

公众号:玩转安卓Dev

Java基础

面向对象与Java基础知识

Java集合框架

JVM

多线程与并发

设计模式

Kotlin

Android

Android基础知识

Android消息机制

Framework

View事件分发机制

Android屏幕刷新机制

View的绘制流程

Activity启动

性能优化

Jetpack&系统View

第三方框架实现原理

计算机网络

算法

其它

Clone this wiki locally