如何在不参考类的情况下找到类的单例对象?

问题描述

让我们考虑以下示例:

import java.util.Map;
import java.util.Optional;

class Scratch {

    public static class UserService {

        private final Map<String,String> users = Map.of(
                "user1","Max","user2","Ivan","user3","Leo");

        public Optional<String> findUserById(String userId) {
            return Optional.ofNullable(users.get(userId));
        }
    }

    // We save our singleton object to this filed
    // to protect from being garbage collected
    private volatile UserService userService = null;

    private UserService getUserService() {
        /// Todo we need to implement it
        /// ObvIoUsly,we cannot return Scratch#userService field
        return null;
    }

    public void doJobs() {
        // I need to get `userService` here,without explicitly passing it
        // to execute some service method

        UserService userService = getUserService();
        if (userService != null) {
            userService.findUserById("userId");
        }
    }

    public void startApplication() {
        userService = new UserService();

        doJobs();
    }

    public static void main(String[] args) {
        Scratch program = new Scratch();
        program.startApplication();
    }
}

所以。我们有一个没有任何框架的简单Java应用程序,例如spring。我需要在UserService方法中找到doJobs()对象,而无需显式传递它。显然,这是工作面试的问题。

有以下任务先决条件:

  • UserService不是Spring bean或类似的东西。与DI
  • 无关
  • 您不能将UserService对象显式传递给doJobs()方法
  • 您无法将UserService对象设置为某些静态/全局变量/接口/方法
  • 您不能使用javaagents。
  • 您知道,当前的类加载器中只有UserService个对象。
  • 如果愿意,您可以使用任何反射(包含的库)
  • 您无法创建新对象,应使用现有对象
  • 您不能将Scratch#userService字段用于任何目的。引入它是为了防止gc。

因此,通常来说,我们需要使用有关类名的知识以某种方式获取所有对象的列表并找到所需的对象。

我在求职面试中没有解决此任务。你能帮我做到吗?

解决方法

也许您错过了一些您未意识到其重要性的问题细节。例如,以下示例适用于HotSpot / OpenJDK和派生的JRE:

import java.lang.ref.Reference;
import java.lang.reflect.*;
import java.util.*;

class Scratch {
    public static class UserService {
        private final Map<String,String> users = Map.of(
            "user1","Max","user2","Ivan","user3","Leo");
        public Optional<String> findUserById(String userId) {
            return Optional.ofNullable(users.get(userId));
        }
        @Override
        protected void finalize() throws Throwable {
            System.out.println("everything went wrong");
        }
    }
    private volatile UserService userService; // never read

    private UserService getUserService() {
        try {
            Class<?> c = Class.forName("java.lang.ref.Finalizer");
            Field[] f={c.getDeclaredField("unfinalized"),c.getDeclaredField("next")};
            AccessibleObject.setAccessible(f,true);
            Reference r = (Reference)f[0].get(null);
            while(r != null) {
                Object o = r.get();
                if(o instanceof UserService) return (UserService)o;
                r = (Reference)f[1].get(r);
            }
        } catch(ReflectiveOperationException ex) {}
        throw new IllegalStateException("was never guaranteed to work anyway");
    }

    public void doJobs() {
        UserService userService = getUserService();
        System.out.println(userService);
        userService.findUserById("userId");
    }

    public void startApplication() {
        userService = new UserService();
        doJobs();
    }
    public static void main(String[] args) {
        Scratch program = new Scratch();
        program.startApplication();
    }
}
Scratch$UserService@9807454

关键的方面是,拥有非平凡的finalize()方法会导致创建特殊引用,该引用可以在不存在对该对象的其他引用的情况下执行终结处理。上面的代码遍历这些特殊引用。

这也提供了一个提示,说明为什么没有其他解决方案(在不阅读此字段的情况下)可以存在。如果该字段包含对该对象的唯一引用,则只有该引用才使实际的,现有的对象与例如恰巧偶然包含相同位模式的大量内存。还是垃圾,即过去曾经碰巧是对象的大量内存,但现在与从未包含对象的内存没有什么不同。

垃圾收集器不关心未使用的内存,无论它是否包含过去的对象,它们都会遍历实时引用以确定可访问的对象。因此,即使您找到了窥视垃圾收集器内部的方法,当垃圾收集器发现现有UserService实例时,您也只是间接读取了字段Scratch.userService,因为这就是垃圾收集器的工作,以发现该对象的存在。

唯一的例外是终结处理,因为它将在没有其他引用的情况下有效地复活该对象以调用finalize()方法,这需要特殊的引用,上述代码已被利用。构造UserService实例时已创建了此附加引用,这是积极使用终结处理使内存管理效率降低的原因之一,How does Java GC call finalize() method?why allocation phase can be increased if we override finalize method?


也就是说,我们必须澄清另外一点:

在这种特定情况下,字段userService不会阻止垃圾收集

这可能与直觉相矛盾,但是如Can java finalize an object when it is still in scope?所述,拥有一个由局部变量引用的对象并不能防止垃圾回收本身。如果随后不使用该变量,则引用的对象可能会被垃圾回收,但是语言规范甚至明确允许代码优化以减少可及性,这可能会导致诸如thisthat或{ {3}}。

在该示例中,Scratch实例仅由局部变量引用,并且在将引用写入userService字段之后,即使没有运行时优化也完全没有使用。甚至要求不要读取该字段,换句话说,不要使用该字段。因此,原则上Scratch实例可以进行垃圾回收。请注意,由于Scratch实例的局部性质,volatile修饰符没有任何意义。即使对象不是纯粹的局部对象,也没有任何读取使它变得毫无意义,尽管优化人员很难识别。因此,由于Scratch实例可以进行垃圾回收,因此仅可收集对象引用的UserService实例也可以。

上面的示例仍然有效,因为它运行的时间不够长,无法进行运行时代码优化或垃圾回收。但是,重要的是要理解,即使有字段,也无法保证对象在内存中仍然存在,因此,通常认为在堆内存中必须找到一种方法是错误的。

,
public static class UserService {

        private final Map<String,String> users = Map.of(
                "user1","Leo");

        public Optional<String> findUserById(String userId) {
            return Optional.ofNullable(users.get(userId));
        }
    }
// We save our singleton object to this filed
    // to protect from being garbage collected
    private volatile UserService userService = new UserService();

    private UserService getUserService() {
        /// TODO we need to implement it
        /// Obviously,we cannot return Scratch#userService field
        return userService;
    }

    public void doJobs() {
        // I need to get `userService` here,without explicitly passing it
        // to execute some service method

        UserService userService = getUserService();
        if (userService != null) {
            System.out.println(userService.findUserById("user1"));
        }
    }

    public void startApplication() {
        userService = new UserService();

        doJobs();
    }

    public static void main(String[] args) {
        Scratch program = new Scratch();
        program.startApplication();
    }
,

任务要求您实现这种方法,而无需直接参考该字段。

private UserService getUserService() {
    return ...;
}

作为实例方法,您可以通过Scratch关键字引用this的实例。使用反射,您可以获取对userService字段的引用,然后使用该字段获取this的该字段的值:

Field field = getClass().getField("userService");
UserService userService = (UserService)field.get(this);

那么完整的方法是:

private UserService getUserService() {
    try {
        return (UserService)getClass().getField("userService").get(this);
    } catch (ReflectiveOperationException e ) {
        throw new RuntimeException(e);
    }
}
,

如果知道df.reset_index(inplace=True) df[['0','1']]=df[['0','1']].apply(lambda x:sorted(x),axis=1).tolist() df 0 1 2 0 a d 8 1 b h 7 2 c f 3 3 c e 3 4 a d 2 5 b b 2 6 c e 1 7 c f 1 8 g i 1 9 b h 1 10 g i 1 df.groupby(['0','1'],as_index=False).sum() 0 1 2 0 a d 10 1 b b 2 2 b h 8 3 c e 4 4 c f 4 5 g i 2 的实例保存在静态字段中,则可以尝试Reflections

UserService

如果该字段不是静态的,那么您将必须创建一个声明类的实例(并代替private UserService getUserService() { Reflections reflections = new Reflections("",new SubTypesScanner(false)); UserService userService = null; for (String name: reflections.getAllTypes()) { try { for (Field field: Class.forName(name).getDeclaredFields()) { if (field.getType().equals(UserService.class)) { userService = (UserService) field.get(null); } } } catch (Exception e) { throw new RuntimeException(e); } } return userService; } 来提供它)

null